From 5353b75c2e5c69f17a0a68e3e1155c67becce855 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 31 Aug 2016 04:19:16 -0500 Subject: [PATCH] posa wip --- admin/app/controllers/artifacts_controller.rb | 2 +- db/manifest | 3 +- db/up/retailers.sql | 96 +++++ ruby/lib/jam_ruby.rb | 5 + ruby/lib/jam_ruby/app/mailers/user_mailer.rb | 35 ++ .../invite_retailer_student.html.erb | 16 + .../invite_retailer_teacher.html.erb | 18 + .../invite_retailer_teacher.text.erb | 11 + .../retailer_customer_blast.html.erb | 8 + .../retailer_customer_blast.text.erb | 6 + ruby/lib/jam_ruby/models/affiliate_partner.rb | 12 + ruby/lib/jam_ruby/models/mix.rb | 1 + ruby/lib/jam_ruby/models/posa_card.rb | 126 ++++++ .../lib/jam_ruby/models/posa_card_purchase.rb | 18 + ruby/lib/jam_ruby/models/posa_card_type.rb | 84 ++++ ruby/lib/jam_ruby/models/retailer.rb | 124 ++++++ .../jam_ruby/models/retailer_invitation.rb | 89 ++++ ruby/lib/jam_ruby/models/sale.rb | 102 +++-- ruby/lib/jam_ruby/models/sale_line_item.rb | 31 +- ruby/lib/jam_ruby/models/teacher.rb | 1 + .../jam_ruby/models/teacher_distribution.rb | 1 + ruby/lib/jam_ruby/models/teacher_payment.rb | 1 + ruby/lib/jam_ruby/models/user.rb | 23 +- ruby/spec/factories.rb | 29 ++ ruby/spec/jam_ruby/models/posa_card_spec.rb | 93 +++++ .../models/retailer_invitation_spec.rb | 33 ++ ruby/spec/jam_ruby/models/retailer_spec.rb | 41 ++ ruby/spec/jam_ruby/models/sale_spec.rb | 36 ++ ruby/spec/mailers/render_emails_spec.rb | 16 + ruby/spec/support/utilities.rb | 4 + web/app/assets/javascripts/accounts.js | 1 + web/app/assets/javascripts/jam_rest.js | 232 ++++++++++- .../assets/javascripts/react-components.js | 1 + .../AccountRetailerScreen.js.jsx.coffee | 390 ++++++++++++++++++ .../InviteSchoolUserDialog.js.jsx.coffee | 4 +- .../actions/RetailerActions.js.coffee | 9 + ...assRetailerLandingBottomPage.js.jsx.coffee | 102 +++++ .../JamClassRetailerLandingPage.js.jsx.coffee | 171 ++++++++ .../JamClassSchoolLandingPage.js.jsx.coffee | 4 +- .../landing/PosaActivationPage.js.jsx.coffee | 196 +++++++++ .../landing/RedeemGiftCardPage.js.jsx.coffee | 28 +- .../RetailerTeacherLandingPage.js.jsx.coffee | 147 +++++++ .../stores/RetailerStore.js.coffee | 65 +++ .../assets/stylesheets/landing/landing.css | 1 + .../landings/individual_jamtrack.scss | 36 ++ .../stylesheets/landings/posa_activation.scss | 62 +++ .../landings/retailer_landing.scss | 204 +++++++++ .../controllers/api_posa_cards_controller.rb | 35 ++ .../api_retailer_invitations_controller.rb | 61 +++ .../controllers/api_retailers_controller.rb | 112 +++++ web/app/controllers/api_users_controller.rb | 261 +++++++----- web/app/controllers/landings_controller.rb | 52 +++ web/app/helpers/sessions_helper.rb | 14 + web/app/views/api_posa_cards/activate.rabl | 3 + web/app/views/api_posa_cards/claim.rabl | 3 + web/app/views/api_posa_cards/show.rabl | 3 + .../api_retailer_invitations/create.rabl | 3 + .../views/api_retailer_invitations/index.rabl | 11 + .../api_retailer_invitations/resend.rabl | 3 + .../views/api_retailer_invitations/show.rabl | 7 + .../views/api_retailers/delete_avatar.rabl | 3 + .../views/api_retailers/remove_teacher.rabl | 3 + web/app/views/api_retailers/show.rabl | 16 + web/app/views/api_retailers/update.rabl | 3 + .../views/api_retailers/update_avatar.rabl | 3 + web/app/views/api_users/show.rabl | 4 + .../views/clients/_account_retailer.html.slim | 9 + web/app/views/clients/index.html.erb | 1 + .../landings/jam_class_retailers.html.slim | 14 + .../views/landings/posa_activation.html.slim | 5 + .../retailer_teacher_register.html.slim | 5 + web/config/routes.rb | 19 + web/lib/google_client.rb | 2 +- .../api_posa_cards_controller_spec.rb | 44 ++ ...pi_retailer_invitations_controller_spec.rb | 54 +++ .../api_retailers_controller_spec.rb | 32 ++ web/spec/factories.rb | 29 ++ web/spec/support/app_config.rb | 5 + 78 files changed, 3356 insertions(+), 181 deletions(-) create mode 100644 db/up/retailers.sql create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_student.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.text.erb create mode 100644 ruby/lib/jam_ruby/models/posa_card.rb create mode 100644 ruby/lib/jam_ruby/models/posa_card_purchase.rb create mode 100644 ruby/lib/jam_ruby/models/posa_card_type.rb create mode 100644 ruby/lib/jam_ruby/models/retailer.rb create mode 100644 ruby/lib/jam_ruby/models/retailer_invitation.rb create mode 100644 ruby/spec/jam_ruby/models/posa_card_spec.rb create mode 100644 ruby/spec/jam_ruby/models/retailer_invitation_spec.rb create mode 100644 ruby/spec/jam_ruby/models/retailer_spec.rb create mode 100644 web/app/assets/javascripts/react-components/AccountRetailerScreen.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/RetailerActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/landing/JamClassRetailerLandingBottomPage.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/landing/JamClassRetailerLandingPage.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/landing/PosaActivationPage.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/landing/RetailerTeacherLandingPage.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/RetailerStore.js.coffee create mode 100644 web/app/assets/stylesheets/landings/posa_activation.scss create mode 100644 web/app/assets/stylesheets/landings/retailer_landing.scss create mode 100644 web/app/controllers/api_posa_cards_controller.rb create mode 100644 web/app/controllers/api_retailer_invitations_controller.rb create mode 100644 web/app/controllers/api_retailers_controller.rb create mode 100644 web/app/views/api_posa_cards/activate.rabl create mode 100644 web/app/views/api_posa_cards/claim.rabl create mode 100644 web/app/views/api_posa_cards/show.rabl create mode 100644 web/app/views/api_retailer_invitations/create.rabl create mode 100644 web/app/views/api_retailer_invitations/index.rabl create mode 100644 web/app/views/api_retailer_invitations/resend.rabl create mode 100644 web/app/views/api_retailer_invitations/show.rabl create mode 100644 web/app/views/api_retailers/delete_avatar.rabl create mode 100644 web/app/views/api_retailers/remove_teacher.rabl create mode 100644 web/app/views/api_retailers/show.rabl create mode 100644 web/app/views/api_retailers/update.rabl create mode 100644 web/app/views/api_retailers/update_avatar.rabl create mode 100644 web/app/views/clients/_account_retailer.html.slim create mode 100644 web/app/views/landings/jam_class_retailers.html.slim create mode 100644 web/app/views/landings/posa_activation.html.slim create mode 100644 web/app/views/landings/retailer_teacher_register.html.slim create mode 100644 web/spec/controllers/api_posa_cards_controller_spec.rb create mode 100644 web/spec/controllers/api_retailer_invitations_controller_spec.rb create mode 100644 web/spec/controllers/api_retailers_controller_spec.rb diff --git a/admin/app/controllers/artifacts_controller.rb b/admin/app/controllers/artifacts_controller.rb index f9ee76b4b..88699cd80 100644 --- a/admin/app/controllers/artifacts_controller.rb +++ b/admin/app/controllers/artifacts_controller.rb @@ -14,7 +14,7 @@ class ArtifactsController < ApplicationController ArtifactUpdate.transaction do # VRFS-1071: Postpone client update notification until installer is available for download ArtifactUpdate.connection.execute('SET TRANSACTION ISOLATION LEVEL READ COMMITTED') - @artifact = ArtifactUpdate.find_or_create_by({product: product, environement: environment}) + @artifact = ArtifactUpdate.find_or_create_by({product: product, environment: environment}) @artifact.version = version @artifact.uri = file diff --git a/db/manifest b/db/manifest index 66680d0e7..3719337d9 100755 --- a/db/manifest +++ b/db/manifest @@ -363,4 +363,5 @@ jamblasters_network.sql immediate_recordings.sql nullable_user_id_jamblaster.sql rails4_migration.sql -non_free_jamtracks.sql \ No newline at end of file +non_free_jamtracks.sql +retailers.sql \ No newline at end of file diff --git a/db/up/retailers.sql b/db/up/retailers.sql new file mode 100644 index 000000000..0a3d564b7 --- /dev/null +++ b/db/up/retailers.sql @@ -0,0 +1,96 @@ +CREATE TABLE retailers ( + id INTEGER PRIMARY KEY, + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + name VARCHAR, + enabled BOOLEAN DEFAULT TRUE, + city VARCHAR, + state VARCHAR, + slug VARCHAR NOT NULL, + encrypted_password VARCHAR NOT NULL DEFAULT uuid_generate_v4(), + photo_url VARCHAR(2048), + original_fpfile VARCHAR(8000), + cropped_fpfile VARCHAR(8000), + cropped_s3_path VARCHAR(8000), + crop_selection VARCHAR(256), + large_photo_url VARCHAR(512), + cropped_large_s3_path VARCHAR(512), + cropped_large_fpfile VARCHAR(8000), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE SEQUENCE retailer_key_sequence; +ALTER SEQUENCE retailer_key_sequence RESTART WITH 10000; +ALTER TABLE retailers ALTER COLUMN id SET DEFAULT nextval('retailer_key_sequence'); + + +CREATE TABLE retailer_invitations ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id), + retailer_id INTEGER REFERENCES retailers(id) NOT NULL, + invitation_code VARCHAR(256) NOT NULL UNIQUE, + note VARCHAR, + email VARCHAR NOT NULL, + first_name VARCHAR, + last_name VARCHAR, + accepted BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE posa_cards ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(64) UNIQUE NOT NULL, + user_id VARCHAR (64) REFERENCES users(id) ON DELETE SET NULL, + card_type VARCHAR(64) NOT NULL, + origin VARCHAR(200), + activated_at TIMESTAMP, + claimed_at TIMESTAMP, + retailer_id INTEGER REFERENCES retailers(id) ON DELETE SET NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX posa_card_user_id_idx ON posa_cards(user_id); + +ALTER TABLE users ADD COLUMN jamclass_credits INTEGER DEFAULT 0; + + +CREATE TABLE posa_card_types ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + card_type VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO posa_card_types (id, card_type) VALUES ('jam_tracks_5', 'jam_tracks_5'); +INSERT INTO posa_card_types (id, card_type) VALUES ('jam_tracks_10', 'jam_tracks_10'); +INSERT INTO posa_card_types (id, card_type) VALUES ('jam_class_10', 'jam_class_10'); + +CREATE TABLE posa_card_purchases ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE SET NULL, + posa_card_type_id VARCHAR(64) REFERENCES posa_card_types(id) ON DELETE SET NULL, + posa_card_id VARCHAR(64) REFERENCES posa_cards(id) ON DELETE SET NULL, + recurly_adjustment_uuid VARCHAR(500), + recurly_adjustment_credit_uuid VARCHAR(500), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE sale_line_items ADD COLUMN posa_card_purchase_id VARCHAR(64) REFERENCES posa_card_purchases(id); + + +ALTER TABLE teachers ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); +ALTER TABLE teachers ADD COLUMN joined_retailer_at TIMESTAMP; +ALTER TABLE retailers ADD jamkazam_rate NUMERIC (8, 2) DEFAULT 0.25; +ALTER TABLE retailers ADD COLUMN affiliate_partner_id INTEGER REFERENCES affiliate_partners(id); +ALTER TABLE lesson_bookings ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); +ALTER TABLE teacher_payments ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); +ALTER TABLE teacher_distributions ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); + + +ALTER TABLE sales ALTER COLUMN user_id DROP NOT NULL; +ALTER TABLE sales ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); +ALTER TABLE sale_line_items ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 45fc7b857..4afe09eb5 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -280,6 +280,9 @@ require "jam_ruby/models/subject" require "jam_ruby/models/band_search" require "jam_ruby/import/tency_stem_mapping" require "jam_ruby/models/jam_track_search" +require "jam_ruby/models/posa_card" +require "jam_ruby/models/posa_card_type" +require "jam_ruby/models/posa_card_purchase" require "jam_ruby/models/gift_card" require "jam_ruby/models/gift_card_purchase" require "jam_ruby/models/gift_card_type" @@ -305,6 +308,8 @@ require "jam_ruby/models/affiliate_distribution" require "jam_ruby/models/teacher_intent" require "jam_ruby/models/school" require "jam_ruby/models/school_invitation" +require "jam_ruby/models/retailer" +require "jam_ruby/models/retailer_invitation" require "jam_ruby/models/teacher_instrument" require "jam_ruby/models/teacher_subject" require "jam_ruby/models/teacher_language" diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index aab962c1f..14b491c2b 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -1698,6 +1698,26 @@ module JamRuby end end + def invite_retailer_teacher(retailer_invitations) + @retailer_invitation = retailer_invitations + @retailer = retailer_invitations.retailer + + email = retailer_invitations.email + @subject = "#{@retailer.owner.name} has sent you an invitation to join #{@retailer.name} on JamKazam" + unique_args = {:type => "invite_retailer_teacher"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + + @suppress_user_has_account_footer = true + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end + end + def invite_school_teacher(school_invitation) @school_invitation = school_invitation @school = school_invitation.school @@ -1882,7 +1902,22 @@ module JamRuby format.text format.html { render :layout => "from_user_mailer" } end + end + def retailer_customer_blast(email, retailer) + @retailer = retailer + @subject = "Check out our teachers at #{@retailer.name}" + unique_args = {:type => "retailer_customer_email"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + + @suppress_user_has_account_footer = true + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end end end end diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_student.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_student.html.erb new file mode 100644 index 000000000..f097b022e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_student.html.erb @@ -0,0 +1,16 @@ +<% provide(:title, @subject) %> + +Hello <%= @retailer_invitation.first_name %> - +

<%= @retailer.owner.first_name %> is using JamKazam to deliver online music lessons, and has sent you this invitation so that you can + register to take online music lessons with <%= @retailer.name %>. To accept this invitation, please click the SIGN UP NOW + button below, and follow the instructions on the web page to which you are taken. Thanks, and on behalf of + <%= @retailer.name %>, welcome to JamKazam!

+
+

+ SIGN + UP NOW +

+
+
+Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.html.erb new file mode 100644 index 000000000..25c0d5ef4 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @subject) %> + +Hello <%= @retailer_invitation.first_name %> - +
+

+ <%= @retailer.owner.first_name %> has set up <%= @retailer.name %> on JamKazam, enabling you to deliver online music + lessons in an amazing new way that really works. To accept this invitation, please click the SIGN UP NOW button below, + and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam!

+
+

+ SIGN + UP NOW +

+ +
+
+Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.text.erb new file mode 100644 index 000000000..f4b5bffe9 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.text.erb @@ -0,0 +1,11 @@ +<% provide(:title, @subject) %> + +Hello <%= @retailer_invitation.first_name %> - +<%= @retailer.owner.first_name %> has set up <%= @retailer.name %> on JamKazam, enabling you to deliver online music +lessons in an amazing new way that really works. To accept this invitation, please click the link below, +and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam! + +<%= @retailer_invitation.generate_signup_url %> + +Best Regards, +Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.html.erb new file mode 100644 index 000000000..204701323 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.html.erb @@ -0,0 +1,8 @@ +<% provide(:title, @subject) %> + +

Click the link of each teacher's profile at <%= @retailer.name %> to find the best fit for you:

+ \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.text.erb new file mode 100644 index 000000000..31f1d6f53 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.text.erb @@ -0,0 +1,6 @@ + +Check out each teacher's profile at <%= @retailer.name %> to find the best fit for you: + +<% @retailer.teachers.each do |teacher| %> +<%= teacher.user.name %>: <%= teacher.user.teacher_profile_url%> +<% end %> diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb index 46c144d9e..d776c2fb9 100644 --- a/ruby/lib/jam_ruby/models/affiliate_partner.rb +++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb @@ -3,6 +3,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id, inverse_of: :affiliate_partner has_one :school, class_name: "JamRuby::School" + has_one :retailer, class_name: "JamRuby::Retailer" has_many :user_referrals, :class_name => "JamRuby::User", :foreign_key => :affiliate_referral_id belongs_to :affiliate_legalese, :class_name => "JamRuby::AffiliateLegalese", :foreign_key => :legalese_id has_many :sale_line_items, :class_name => 'JamRuby::SaleLineItem', foreign_key: :affiliate_referral_id @@ -94,6 +95,17 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base oo.save end + def self.create_from_retailer(retailer) + oo = AffiliatePartner.new + oo.partner_name = "Affiliate from Retailer #{retailer.id}" + oo.partner_user = retailer.owner + oo.entity_type = 'Other' + oo.retailer = retailer + oo.signed_at = nil + oo.save + end + + def self.coded_id(code=nil) self.where(:partner_code => code).limit(1).pluck(:id).first if code.present? end diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb index 572d137a1..9ced0fe74 100644 --- a/ruby/lib/jam_ruby/models/mix.rb +++ b/ruby/lib/jam_ruby/models/mix.rb @@ -76,6 +76,7 @@ module JamRuby end def can_download?(some_user) + return false if some_user.nil? claimed_recording = ClaimedRecording.find_by_user_id_and_recording_id(some_user.id, recording.id) if claimed_recording diff --git a/ruby/lib/jam_ruby/models/posa_card.rb b/ruby/lib/jam_ruby/models/posa_card.rb new file mode 100644 index 000000000..58028ed5a --- /dev/null +++ b/ruby/lib/jam_ruby/models/posa_card.rb @@ -0,0 +1,126 @@ +# represents the gift card you hold in your hand +module JamRuby + class PosaCard < ActiveRecord::Base + + @@log = Logging.logger[PosaCard] + + JAM_TRACKS_5 = 'jam_tracks_5' + JAM_TRACKS_10 = 'jam_tracks_10' + JAM_CLASS_4 = 'jam_tracks_4' + CARD_TYPES = + [ + JAM_TRACKS_5, + JAM_TRACKS_10, + JAM_CLASS_4 + ] + + + belongs_to :user, class_name: "JamRuby::User" + belongs_to :retailer, class_name: "JamRuby::Retailer" + has_many :posa_card_purchases, class_name: 'JamRuby::PosaCardPurchase' + + validates :card_type, presence: true, inclusion: {in: CARD_TYPES} + validates :code, presence: true, uniqueness: true + + after_save :check_attributed + + validate :already_activated + validate :retailer_set + validate :already_claimed + validate :user_set + validate :must_be_activated + validate :within_one_year + + def already_activated + if activated_at && activated_at_was && activated_at_changed? + if retailer && retailer_id == retailer_id_was + self.errors.add(:activated_at, 'already activated. Please give card to customer. Thank you!') + else + self.errors.add(:activated_at, 'already activated by someone else. Please contact support@jamkaazm.com') + end + end + end + + def within_one_year + if user && claimed_at && claimed_at_was && claimed_at_changed? + if !user.can_claim_posa_card + self.errors.add(:claimed_at, 'was within 1 year') + end + end + end + + def already_claimed + if claimed_at && claimed_at_was && claimed_at_changed? + self.errors.add(:claimed_at, 'already claimed') + end + end + + def retailer_set + if activated_at && !retailer + self.errors.add(:retailer, 'must be specified') + end + end + + def user_set + if claimed_at && !user + self.errors.add(:user, 'must be specified') + end + end + + def must_be_activated + if claimed_at && !activated_at + self.errors.add(:activated_at, 'must already be set') + end + end + + def check_attributed + if user && user_id_changed? + if card_type == JAM_TRACKS_5 + user.gifted_jamtracks += 5 + elsif card_type == JAM_TRACKS_10 + user.gifted_jamtracks += 10 + elsif card_type == JAM_CLASS_4 + user.jamclass_credits += 4 + else + raise "unknown card type #{card_type}" + end + user.save! + end + end + + def product_info + price = nil + plan_code = nil + + if card_type == JAM_TRACKS_5 + price = 9.99 + plan_code = 'posa-jamtracks-5' + elsif card_type == JAM_TRACKS_10 + price = 19.99 + plan_code = 'posa-jatracks-10' + elsif card_type == JAM_CLASS_4 + price = 49.99 + plan_code = 'posa-jamclass-4' + else + raise "unknown card type #{card_type}" + end + {price: price, quantity: 1, marked_for_redeem: false, plan_code: plan_code} + end + + def self.activate(posa_card, retailer) + Sale.posa_activate(posa_card, retailer) + end + + def activate(retailer) + self.activated_at = Time.now + self.retailer = retailer + self.save + end + + def claim(user) + self.user = user + self.claimed_at = Time.now + self.save + end + end +end diff --git a/ruby/lib/jam_ruby/models/posa_card_purchase.rb b/ruby/lib/jam_ruby/models/posa_card_purchase.rb new file mode 100644 index 000000000..4a5356188 --- /dev/null +++ b/ruby/lib/jam_ruby/models/posa_card_purchase.rb @@ -0,0 +1,18 @@ +# reperesents the gift card you buy from the site (but physical gift card is modeled by GiftCard) +module JamRuby + class PosaCardPurchase < ActiveRecord::Base + + @@log = Logging.logger[PosaCardPurchase] + + attr_accessible :user, :posa_card_type + + def name + posa_card_type.sale_display + end + + # who purchased the card? + belongs_to :user, class_name: "JamRuby::User" + belongs_to :posa_card_type, class_name: "JamRuby::PosaCardType" + belongs_to :posa_card, class_name: 'JamRuby::PosaCard' + end +end diff --git a/ruby/lib/jam_ruby/models/posa_card_type.rb b/ruby/lib/jam_ruby/models/posa_card_type.rb new file mode 100644 index 000000000..a278d1f00 --- /dev/null +++ b/ruby/lib/jam_ruby/models/posa_card_type.rb @@ -0,0 +1,84 @@ +# reperesents the posa card you buy from the site +module JamRuby + class PosaCardType < ActiveRecord::Base + + @@log = Logging.logger[PosaCardType] + + PRODUCT_TYPE = 'PosaCardType' + + + JAM_TRACKS_5 = 'jam_tracks_5' + JAM_TRACKS_10 = 'jam_tracks_10' + JAM_CLASS_4 = 'jam_tracks_4' + CARD_TYPES = + [ + JAM_TRACKS_5, + JAM_TRACKS_10, + JAM_CLASS_4 + ] + + validates :card_type, presence: true, inclusion: {in: CARD_TYPES} + + def self.jam_track_5 + PosaCardType.find(JAM_TRACKS_5) + end + + def self.jam_track_10 + PosaCardType.find(JAM_TRACKS_10) + end + + def self.jam_class_4 + PosaCardType.find(JAM_CLASS_4) + end + + + def name + sale_display + end + + def price + if card_type == JAM_TRACKS_5 + 10.00 + elsif card_type == JAM_TRACKS_10 + 20.00 + elsif card_type == JAM_CLASS_4 + 49.99 + else + raise "unknown card type #{card_type}" + end + end + + + def sale_display + if card_type == JAM_TRACKS_5 + 'JamTracks Card (5)' + elsif card_type == JAM_TRACKS_10 + 'JamTracks Card (10)' + elsif card_type == JAM_TRACKS_10 + 'JamClass Card (4)' + else + raise "unknown card type #{card_type}" + end + end + + def plan_code + if card_type == JAM_TRACKS_5 + "jamtrack-posacard-5" + elsif card_type == JAM_TRACKS_10 + "jamtrack-posacard-10" + elsif card_type == JAM_CLASS_4 + "jamclass-posacard-4" + else + raise "unknown card type #{card_type}" + end + end + + def sales_region + 'Worldwide' + end + + def to_s + sale_display + end + end +end diff --git a/ruby/lib/jam_ruby/models/retailer.rb b/ruby/lib/jam_ruby/models/retailer.rb new file mode 100644 index 000000000..40169e5ec --- /dev/null +++ b/ruby/lib/jam_ruby/models/retailer.rb @@ -0,0 +1,124 @@ +module JamRuby + class Retailer < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:name] + + attr_accessor :updating_avatar, :password, :should_validate_password + attr_accessible :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection + + belongs_to :posa_cards, class_name: 'JamRuby::PosaCard' + belongs_to :user, class_name: ::JamRuby::User, inverse_of: :owned_retailer + belongs_to :affiliate_partner, class_name: "JamRuby::AffiliatePartner" + has_many :teachers, class_name: "JamRuby::Teacher" + has_many :retailer_invitations, class_name: 'JamRuby::RetailerInvitation' + has_many :teacher_payments, class_name: 'JamRuby::TeacherPayment' + has_many :teacher_distributions, class_name: 'JamRuby::TeacherDistribution' + has_many :sales, class_name: 'JamRuby::Sale' + has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem' + + validates :user, presence: true + validates :slug, presence: true + validates :enabled, inclusion: {in: [true, false]} + validates_length_of :password, minimum: 6, maximum: 100, :if => :should_validate_password + + after_create :create_affiliate + before_save :stringify_avatar_info, :if => :updating_avatar + + def create_affiliate + AffiliatePartner.create_from_retailer(self) + end + + def encrypt(password) + BCrypt::Password.create(password, cost: 12).to_s + end + + def matches_password(password) + BCrypt::Password.new(self.encrypted_password) == password + end + + def update_from_params(params) + self.name = params[:name] if params[:name].present? + self.city = params[:city] if params[:city].present? + self.city = params[:state] if params[:state].present? + self.slug = params[:slug] if params[:slug].present? + + if params[:password].present? + self.should_validate_password = true + self.password = params[:password] + self.encrypted_password = encrypt(params[:password]) + end + self.save + end + + def owner + user + end + + def validate_avatar_info + if updating_avatar + # we want to mak sure that original_fpfile and cropped_fpfile seems like real fpfile info objects (i.e, json objects from filepicker.io) + errors.add(:original_fpfile, ValidationMessages::INVALID_FPFILE) if self.original_fpfile.nil? || self.original_fpfile["key"].nil? || self.original_fpfile["url"].nil? + errors.add(:cropped_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_fpfile.nil? || self.cropped_fpfile["key"].nil? || self.cropped_fpfile["url"].nil? + errors.add(:cropped_large_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_large_fpfile.nil? || self.cropped_large_fpfile["key"].nil? || self.cropped_large_fpfile["url"].nil? + end + end + + def escape_filename(path) + dir = File.dirname(path) + file = File.basename(path) + "#{dir}/#{ERB::Util.url_encode(file)}" + end + + def update_avatar(original_fpfile, cropped_fpfile, cropped_large_fpfile, crop_selection, aws_bucket) + self.updating_avatar = true + + cropped_s3_path = cropped_fpfile["key"] + cropped_large_s3_path = cropped_large_fpfile["key"] + + self.update_attributes( + :original_fpfile => original_fpfile, + :cropped_fpfile => cropped_fpfile, + :cropped_large_fpfile => cropped_large_fpfile, + :cropped_s3_path => cropped_s3_path, + :cropped_large_s3_path => cropped_large_s3_path, + :crop_selection => crop_selection, + :photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => true), + :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true) + ) + end + + def delete_avatar(aws_bucket) + + User.transaction do + + unless self.cropped_s3_path.nil? + S3Util.delete(aws_bucket, File.dirname(self.cropped_s3_path) + '/cropped.jpg') + S3Util.delete(aws_bucket, self.cropped_s3_path) + S3Util.delete(aws_bucket, self.cropped_large_s3_path) + end + + return self.update_attributes( + :original_fpfile => nil, + :cropped_fpfile => nil, + :cropped_large_fpfile => nil, + :cropped_s3_path => nil, + :cropped_large_s3_path => nil, + :photo_url => nil, + :crop_selection => nil, + :large_photo_url => nil + ) + end + end + + def stringify_avatar_info + # fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR, + # so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object) + # later, when serving this data out to the REST API, we currently just leave it as a string and make a JSON capable + # client parse it, because it's very rare when it's needed at all + self.original_fpfile = original_fpfile.to_json if !original_fpfile.nil? + self.cropped_fpfile = cropped_fpfile.to_json if !cropped_fpfile.nil? + self.crop_selection = crop_selection.to_json if !crop_selection.nil? + end + end +end diff --git a/ruby/lib/jam_ruby/models/retailer_invitation.rb b/ruby/lib/jam_ruby/models/retailer_invitation.rb new file mode 100644 index 000000000..dad1f18f2 --- /dev/null +++ b/ruby/lib/jam_ruby/models/retailer_invitation.rb @@ -0,0 +1,89 @@ +module JamRuby + class RetailerInvitation < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:note] + + + belongs_to :user, class_name: ::JamRuby::User + belongs_to :retailer, class_name: ::JamRuby::Retailer + + validates :retailer, presence: true + validates :email, email: true + validates :invitation_code, presence: true + validates :accepted, inclusion: {in: [true, false]} + validates :first_name, presence: true + validates :last_name, presence: true + validate :retailer_has_name, on: :create + + before_validation(on: :create) do + self.invitation_code = SecureRandom.urlsafe_base64 if self.invitation_code.nil? + end + + def retailer_has_name + if retailer && retailer.name.blank? + errors.add(:retailer, "must have name") + end + end + + def self.index(retailer, params) + limit = params[:per_page] + limit ||= 100 + limit = limit.to_i + + query = RetailerInvitation.where(retailer_id: retailer.id) + query = query.includes([:user, :retailer]) + query = query.order('created_at') + query = query.where(accepted:false) + + + current_page = params[:page].nil? ? 1 : params[:page].to_i + next_page = current_page + 1 + + # will_paginate gem + query = query.paginate(:page => current_page, :per_page => limit) + + if query.length == 0 # no more results + {query: query, next_page: nil} + elsif query.length < limit # no more results + {query: query, next_page: nil} + else + {query: query, next_page: next_page} + end + end + + + def self.create(current_user, specified_retailer, params) + + invitation = RetailerInvitation.new + invitation.retailer = specified_retailer + invitation.email = params[:email] + invitation.first_name = params[:first_name] + invitation.last_name = params[:last_name] + + if invitation.save + invitation.send_invitation + end + invitation + end + + + def send_invitation + UserMailer.invite_retailer_teacher(self).deliver_now + end + def generate_signup_url + "#{APP_CONFIG.external_root_url}/retailer/#{retailer.id}/teacher?invitation_code=#{self.invitation_code}" + end + + def delete + self.destroy + end + + def resend + send_invitation + end + + + + end +end diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 37dc846af..0d1b4ca89 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -5,17 +5,20 @@ module JamRuby JAMTRACK_SALE = 'jamtrack' LESSON_SALE = 'lesson' + POSA_SALE = 'posacard' SOURCE_RECURLY = 'recurly' SOURCE_IOS = 'ios' + belongs_to :retailer, class_name: 'JamRuby::Retailer' belongs_to :user, class_name: 'JamRuby::User' has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem' has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale, foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id' validates :order_total, numericality: {only_integer: false} - validates :user, presence: true + #validates :user + #validates :retailer @@log = Logging.logger[Sale] @@ -215,6 +218,26 @@ module JamRuby def self.post_sale_test_failure return true end + + def self.posa_activate(posa_card, retailer) + sale = nil + Sale.transaction(:requires_new => true) do + + posa_card.activate(retailer) + + if !posa_card.errors.any? + + sale = create_posa_sale(retailer, posa_card) + + SaleLineItem.create_from_posa_card(sale, retailer, posa_card) + + sale.save + end + + end + {sale: sale} + end + # this is easy to make generic, but right now, it just purchases lessons def self.purchase_lesson(charge, current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil, force = false) stripe_charge = nil @@ -223,43 +246,43 @@ module JamRuby # everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it Sale.transaction(:requires_new => true) do - sale = create_lesson_sale(current_user) + sale = create_lesson_sale(current_user) - if sale.valid? + if sale.valid? - if lesson_booking - lesson_booking.current_lesson = lesson_session - lesson_booking.current_purchase = lesson_package_purchase - end - - sale_line_item = SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking) - - price_info = charge_stripe_for_lesson(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force) - - post_sale_test_failure - - if price_info[:purchase] && price_info[:purchase].errors.any? - purchase = price_info[:purchase] - raise ActiveRecord::Rollback - end - - if !sale_line_item.valid? - raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}" - end - # sale.source = 'stripe' - sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents] - sale.recurly_tax_in_cents = price_info[:tax_in_cents] - sale.recurly_total_in_cents = price_info[:total_in_cents] - sale.recurly_currency = price_info[:currency] - sale.stripe_charge_id = price_info[:charge_id] - sale.save - stripe_charge = price_info[:charge] - purchase = price_info[:purchase] - else - # should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point. - puts "invalid sale object" - raise "invalid sale object" + if lesson_booking + lesson_booking.current_lesson = lesson_session + lesson_booking.current_purchase = lesson_package_purchase end + + sale_line_item = SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking) + + price_info = charge_stripe_for_lesson(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force) + + post_sale_test_failure + + if price_info[:purchase] && price_info[:purchase].errors.any? + purchase = price_info[:purchase] + raise ActiveRecord::Rollback + end + + if !sale_line_item.valid? + raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}" + end + # sale.source = 'stripe' + sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents] + sale.recurly_tax_in_cents = price_info[:tax_in_cents] + sale.recurly_total_in_cents = price_info[:total_in_cents] + sale.recurly_currency = price_info[:currency] + sale.stripe_charge_id = price_info[:charge_id] + sale.save + stripe_charge = price_info[:charge] + purchase = price_info[:purchase] + else + # should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point. + puts "invalid sale object" + raise "invalid sale object" + end end {sale: sale, stripe_charge: stripe_charge, purchase: purchase} @@ -634,6 +657,15 @@ module JamRuby sale end + def self.create_posa_sale(retailer, posa_card) + sale = Sale.new + sale.retailer = retailer + sale.sale_type = POSA_SALE # gift cards and jam tracks are sold with this type of sale + sale.order_total = 0 + sale.save + sale + end + # this checks just jamtrack sales appropriately def self.check_integrity_of_jam_track_sales Sale.select([:total, :voided]).find_by_sql( diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index 7dec6502d..b59124a52 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -6,12 +6,14 @@ module JamRuby JAMTRACK = 'JamTrack' GIFTCARD = 'GiftCardType' LESSON = 'LessonPackageType' + POSACARD = 'PosaCard' belongs_to :sale, class_name: 'JamRuby::Sale' belongs_to :jam_track, class_name: 'JamRuby::JamTrack' belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight' belongs_to :gift_card, class_name: 'JamRuby::GiftCard' belongs_to :lesson_package_purchase, class_name: 'JamRuby::LessonPackagePurchase' + belongs_to :retailer, class_name: 'JamRuby::Retailer' # deprecated; use affiliate_distribution !! belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id @@ -20,7 +22,7 @@ module JamRuby has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid' - validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD, LESSON]} + validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD, LESSON, POSACARD]} validates :unit_price, numericality: {only_integer: false} validates :quantity, numericality: {only_integer: true} validates :free, numericality: {only_integer: true} @@ -128,6 +130,33 @@ module JamRuby line_item end + def self.create_from_posa_card(sale, retailer, posa_card) + product_info = posa_card.product_info + sale_line_item = SaleLineItem.new + sale_line_item.retailer = retailer + sale_line_item.product_type = POSACARD + sale_line_item.product_id = posa_card.id + sale_line_item.unit_price = product_info[:price] + sale_line_item.quantity = product_info[:quantity] + sale_line_item.free = product_info[:marked_for_redeem] + sale_line_item.sales_tax = nil + sale_line_item.shipping_handling = 0 + sale_line_item.recurly_plan_code = product_info[:plan_code] + + + #referral_info = retailer.referral_info + + #if referral_info + # sale_line_item.affiliate_distributions << AffiliateDistribution.create(retailer.affiliate_partner, referral_info[:fee_in_cents], sale_line_item) + # sale_line_item.affiliate_referral = retailer.affiliate_partner + # sale_line_item.affiliate_referral_fee_in_cents = referral_info[:fee_in_cents] + #end + + sale.sale_line_items << sale_line_item + sale_line_item.save + sale_line_item + end + def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid, instance = nil) product_info = shopping_cart.product_info(instance) diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb index ec1cc7d38..14d1fe2f5 100644 --- a/ruby/lib/jam_ruby/models/teacher.rb +++ b/ruby/lib/jam_ruby/models/teacher.rb @@ -22,6 +22,7 @@ module JamRuby has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target has_one :user, :class_name => 'JamRuby::User', foreign_key: :teacher_id belongs_to :school, :class_name => "JamRuby::School", inverse_of: :teachers + belongs_to :retailer, :class_name => "JamRuby::Retailer", inverse_of: :teachers validates :user, :presence => true validates :biography, length: {minimum: 5, maximum: 4096}, :if => :validate_introduction diff --git a/ruby/lib/jam_ruby/models/teacher_distribution.rb b/ruby/lib/jam_ruby/models/teacher_distribution.rb index 0b82910b7..5d93d3581 100644 --- a/ruby/lib/jam_ruby/models/teacher_distribution.rb +++ b/ruby/lib/jam_ruby/models/teacher_distribution.rb @@ -6,6 +6,7 @@ module JamRuby belongs_to :lesson_session, class_name: "JamRuby::LessonSession" belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase" belongs_to :school, class_name: "JamRuby::School" + belongs_to :retailer, class_name: "JamRuby::Retailer" validates :teacher, presence: true validates :amount_in_cents, presence: true diff --git a/ruby/lib/jam_ruby/models/teacher_payment.rb b/ruby/lib/jam_ruby/models/teacher_payment.rb index 94f2df67c..25ac5f614 100644 --- a/ruby/lib/jam_ruby/models/teacher_payment.rb +++ b/ruby/lib/jam_ruby/models/teacher_payment.rb @@ -5,6 +5,7 @@ module JamRuby belongs_to :teacher_payment_charge, class_name: "JamRuby::TeacherPaymentCharge", foreign_key: :charge_id has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" belongs_to :school, class_name: "JamRuby::School" + belongs_to :retailer, class_name: "JamRuby::Retailer" def self.hourly_check diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 7d67b938e..4faf1d925 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -204,11 +204,13 @@ module JamRuby has_many :taught_lessons, :class_name => "JamRuby::LessonSession", inverse_of: :teacher, foreign_key: :teacher_id belongs_to :school, :class_name => "JamRuby::School", inverse_of: :students has_one :owned_school, :class_name => "JamRuby::School", inverse_of: :user + has_one :owned_retailer, :class_name => "JamRuby::Retailer", inverse_of: :user has_many :test_drive_package_choices, :class_name =>"JamRuby::TestDrivePackageChoice" has_many :jamblasters_users, class_name: "JamRuby::JamblasterUser" has_many :jamblasters, class_name: 'JamRuby::Jamblaster', through: :jamblasters_users has_many :proposed_slots, class_name: 'JamRuby::LessonBookingSlot', inverse_of: :proposer, dependent: :destroy, foreign_key: :proposer_id has_many :charges, class_name: 'JamRuby::Charge', dependent: :destroy + has_many :posa_cards, class_name: 'JamRuby::PosaCard', dependent: :destroy before_save :default_anonymous_names before_save :create_remember_token, :if => :should_validate_password? @@ -1157,6 +1159,7 @@ module JamRuby user.terms_of_service = terms_of_service user.reuse_card unless reuse_card.nil? user.gifted_jamtracks = 0 + user.jamclass_credits = 0 user.has_redeemable_jamtrack = true user.is_a_student = !!student user.is_a_teacher = !!teacher @@ -1304,9 +1307,18 @@ module JamRuby # if a gift card value was passed in, then try to find that gift card and apply it to user if gift_card - user.expecting_gift_card = true - found_gift_card = GiftCard.where(code: gift_card).where(user_id: nil).first - user.gift_cards << found_gift_card if found_gift_card + + # first try posa card + posa_card = PosaCard.where(code: gift_card) + + if posa_card + posa_card.claim(user) + user.posa_cards << posa_card + else + user.expecting_gift_card = true + found_gift_card = GiftCard.where(code: gift_card).where(user_id: nil).first + user.gift_cards << found_gift_card if found_gift_card + end end user.save @@ -1961,6 +1973,11 @@ module JamRuby lesson_purchases.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).where('created_at > ?', APP_CONFIG.test_drive_wait_period_year.years.ago).count == 0 end + # validate if within waiting period + def can_claim_posa_card + posa_cards.where('card_type = ?', PosaCard::JAM_CLASS_4).where('claimed_at > ?', APP_CONFIG.jam_class_card_wait_period_year.years.ago).count == 0 + end + def lessons_with_teacher(teacher) taken_lessons.where(teacher_id: teacher.id) end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 2c442e074..59803b48d 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -898,6 +898,19 @@ FactoryGirl.define do association :user, factory: :user end + factory :posa_card, class: 'JamRuby::PosaCard' do + sequence(:code) { |n| n.to_s } + card_type JamRuby::PosaCardType::JAM_TRACKS_5 + end + + factory :posa_card_type, class: 'JamRuby::PosaCardType' do + card_type JamRuby::PosaCardType::JAM_TRACKS_5 + end + + factory :posa_card_purchase, class: 'JamRuby::PosaCardPurchase' do + association :user, factory: :user + end + factory :jamblaster, class: 'JamRuby::Jamblaster' do association :user, factory: :user @@ -932,6 +945,22 @@ FactoryGirl.define do accepted false end + factory :retailer, class: 'JamRuby::Retailer' do + association :user, factory: :user + sequence(:name) { |n| "Dat Music Retailer" } + sequence(:slug) { |n| "retailer-#{n}" } + enabled true + end + + factory :retailer_invitation, class: 'JamRuby::RetailerInvitation' do + association :retailer, factory: :retailer + note "hey come in in" + sequence(:email) { |n| "retail_person#{n}@example.com" } + sequence(:first_name) { |n| "FirstName" } + sequence(:last_name) { |n| "LastName" } + accepted false + end + factory :lesson_booking_slot, class: 'JamRuby::LessonBookingSlot' do factory :lesson_booking_slot_single do slot_type 'single' diff --git a/ruby/spec/jam_ruby/models/posa_card_spec.rb b/ruby/spec/jam_ruby/models/posa_card_spec.rb new file mode 100644 index 000000000..d9cef7984 --- /dev/null +++ b/ruby/spec/jam_ruby/models/posa_card_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe PosaCard do + + let(:user) {FactoryGirl.create(:user)} + let(:card) {FactoryGirl.create(:posa_card)} + let(:card2) {FactoryGirl.create(:posa_card)} + let(:retailer) {FactoryGirl.create(:retailer)} + it "created by factory" do + card.touch + end + + describe "activated" do + it "succeeds" do + card.activate(retailer) + + card.errors.any?.should be false + card.activated_at.should_not be_nil + card.retailer.should eql retailer + end + + it "cant be re-activated" do + + card.activate(retailer) + + Timecop.travel(Time.now + 1) + + card.activate(retailer) + + card.errors.any?.should be true + card.errors[:activated_at].should eql ['already activated. Please give card to customer. Thank you!'] + end + + it "must have retailer" do + + card.activate(nil) + + card.errors.any?.should be true + card.errors[:retailer].should eql ['must be specified'] + end + end + + describe "claim" do + it "succeeds" do + card.activate(retailer) + card.claim(user) + + card.errors.any?.should be false + card.claimed_at.should_not be_nil + card.user.should eql user + end + + it "must be already activated" do + + card.claim(user) + + card.errors.any?.should be true + card.errors[:activated_at].should eql ['must already be set'] + end + + it "cant be re-claimed" do + + card.activate(retailer) + card.claim(user) + + Timecop.travel(Time.now + 1) + + card.claim(user) + + card.errors.any?.should be true + card.errors[:claimed_at].should eql ['already claimed'] + end + + it "must have user" do + card.activate(retailer) + card.claim(nil) + + card.errors.any?.should be true + card.errors[:user].should eql ['must be specified'] + end + + it "can't be within one year" do + card.activate(retailer) + card.claim(user) + + card2.activate(retailer) + card2.claim(user) + + card2.errors.any?.should be true + card2.errors[:user].should eql ['was within 1 year'] + end + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/retailer_invitation_spec.rb b/ruby/spec/jam_ruby/models/retailer_invitation_spec.rb new file mode 100644 index 000000000..4e1cee8f3 --- /dev/null +++ b/ruby/spec/jam_ruby/models/retailer_invitation_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe RetailerInvitation do + + let(:retailer) {FactoryGirl.create(:retailer)} + + it "created by factory" do + FactoryGirl.create(:retailer_invitation) + end + + it "created by method" do + RetailerInvitation.create(retailer.user, retailer, {first_name: "Bobby", last_name: "Jimes", email: 'somewhere@jamkazam.com'}) + end + + describe "index" do + it "works" do + RetailerInvitation.index(retailer, {})[:query].count.should eql 0 + + FactoryGirl.create(:retailer_invitation) + RetailerInvitation.index(retailer, {})[:query].count.should eql 0 + RetailerInvitation.index(retailer, {})[:query].count.should eql 0 + + FactoryGirl.create(:retailer_invitation, retailer: retailer, ) + RetailerInvitation.index(retailer, {})[:query].count.should eql 1 + + FactoryGirl.create(:retailer_invitation, retailer: retailer, ) + RetailerInvitation.index(retailer, {})[:query].count.should eql 2 + + end + + + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/retailer_spec.rb b/ruby/spec/jam_ruby/models/retailer_spec.rb new file mode 100644 index 000000000..edd56bc20 --- /dev/null +++ b/ruby/spec/jam_ruby/models/retailer_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Retailer do + + it "created by factory" do + FactoryGirl.create(:retailer) + end + + it "has correct associations" do + retailer = FactoryGirl.create(:retailer) + + retailer.should eql retailer.user.owned_retailer + + teacher = FactoryGirl.create(:teacher, retailer: retailer) + + retailer.reload + retailer.teachers.to_a.should eql [teacher] + + teacher.retailer.should eql retailer + end + + it "updates" do + retailer = FactoryGirl.create(:retailer) + retailer.update_from_params({name: 'hahah'}) + retailer.errors.any?.should be false + end + + it "updates password" do + retailer = FactoryGirl.create(:retailer) + retailer.update_from_params({name: 'hahah', password: 'abc'}) + retailer.errors.any?.should be true + retailer.errors[:password].should eql ['is too short (minimum is 6 characters)'] + + retailer.update_from_params({name: 'hahah', password: 'abcdef'}) + retailer.errors.any?.should be false + retailer.matches_password('abcdef').should be true + + retailer = Retailer.find_by_id(retailer.id) + retailer.matches_password('abcdef').should be true + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index 1a68e9910..8c035a50a 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -8,6 +8,8 @@ describe Sale do let(:jam_track2) { FactoryGirl.create(:jam_track) } let(:jam_track3) { FactoryGirl.create(:jam_track) } let(:gift_card) { GiftCardType.jam_track_5 } + let(:posa_card) {FactoryGirl.create(:posa_card)} + let(:retailer) {FactoryGirl.create(:retailer)} after(:each) { Timecop.return @@ -27,6 +29,39 @@ describe Sale do sale_line_item.product_id.should eq(jamtrack.id) end + describe "posa_cards" do + it "works" do + posa_card.card_type.should eql PosaCard::JAM_TRACKS_5 + + result = Sale.posa_activate(posa_card, retailer) + posa_card.errors.any?.should be false + posa_card.activated_at.should_not be_nil + sale = result[:sale] + + sale = Sale.find(sale.id) + sale.sale_line_items.count.should eql 1 + sale_line_item = sale.sale_line_items.first + sale.retailer.should eql retailer + sale.sale_type.should eql Sale::POSA_SALE + sale_line_item.retailer.should eql retailer + sale_line_item.unit_price.should eql 9.99 # + sale_line_item.quantity.should eql 1 + end + + it "already activated" do + result = Sale.posa_activate(posa_card, retailer) + posa_card.activated_at.should_not be_nil + posa_card.errors.any?.should be false + sale = result[:sale] + + result2 = Sale.posa_activate(posa_card, retailer) + posa_card.activated_at.should_not be_nil + posa_card.errors.any?.should be true + result2[:sale].should be_nil + posa_card.errors[:activated_at].should eq ["already activated. Please give card to customer. Thank you!"] + end + end + describe "index" do it "empty" do result = Sale.index(user) @@ -941,6 +976,7 @@ describe Sale do r.voided.to_i.should eq(1) end + end end diff --git a/ruby/spec/mailers/render_emails_spec.rb b/ruby/spec/mailers/render_emails_spec.rb index 2f7cc727c..8166798da 100644 --- a/ruby/spec/mailers/render_emails_spec.rb +++ b/ruby/spec/mailers/render_emails_spec.rb @@ -210,6 +210,22 @@ describe "RenderMailers", :slow => true do end end + describe "Retailer emails" do + let(:retailer) {FactoryGirl.create(:retailer)} + + before(:each) do + UserMailer.deliveries.clear + end + after(:each) do + UserMailer.deliveries.length.should == 1 + # NOTE! we take the second email, because the act of creating the InvitedUser model + # sends an email too, before our it {} block runs. This is because we have an InvitedUserObserver + mail = UserMailer.deliveries[0] + save_emails_to_disk(mail, @filename) + end + it {@filename="retailer_customer_blast"; UserMailer.retailer_customer_blast('seth@jamkazam.com', retailer).deliver_now} + end + describe "InvitedUserMailer emails" do let(:user2) { FactoryGirl.create(:user) } diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index ca203612f..0a4c93c00 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -294,6 +294,10 @@ def app_config 1 end + def jam_class_card_wait_period_year + 1 + end + def check_bounced_emails false end diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index 6f144f06a..6f6e189f3 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -80,6 +80,7 @@ affiliate_earnings: (userDetail.affiliate_earnings / 100).toFixed(2), affiliate_referral_count: userDetail.affiliate_referral_count, owns_school: !!userDetail.owned_school_id, + owns_retailer: !!userDetail.owned_retailer_id, webcamName: webcamName } , { variable: 'data' })); diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 6475b0ff5..4e74551d1 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -2367,8 +2367,6 @@ }) } - - function updateSchoolAvatar(options) { var id = getId(options); @@ -2488,6 +2486,171 @@ }); } + function deleteSchoolTeacher(options) { + + var id = getId(options); + + return $.ajax({ + type: "DELETE", + url: "/api/schools/" + id + '/teachers/' + options.teacher_id, + dataType: "json", + contentType: 'application/json' + }); + } + + function getRetailer(options) { + + var id = getId(options); + return $.ajax({ + type: "GET", + url: "/api/retailers/" + id, + dataType: "json", + contentType: 'application/json' + }); + } + + function updateRetailer(options) { + var id = getId(options); + return $.ajax({ + type: "POST", + url: '/api/retailers/' + id, + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function updateRetailerAvatar(options) { + var id = getId(options); + + var original_fpfile = options['original_fpfile']; + var cropped_fpfile = options['cropped_fpfile']; + var cropped_large_fpfile = options['cropped_large_fpfile']; + var crop_selection = options['crop_selection']; + + logger.debug(JSON.stringify({ + original_fpfile : original_fpfile, + cropped_fpfile : cropped_fpfile, + cropped_large_fpfile : cropped_large_fpfile, + crop_selection : crop_selection + })); + + var url = "/api/retailers/" + id + "/avatar"; + return $.ajax({ + type: "POST", + dataType: "json", + url: url, + contentType: 'application/json', + processData:false, + data: JSON.stringify({ + original_fpfile : original_fpfile, + cropped_fpfile : cropped_fpfile, + cropped_large_fpfile : cropped_large_fpfile, + crop_selection : crop_selection + }) + }); + } + + function deleteRetailerAvatar(options) { + var id = getId(options); + + var url = "/api/retailers/" + id + "/avatar"; + return $.ajax({ + type: "DELETE", + dataType: "json", + url: url, + contentType: 'application/json', + processData:false + }); + } + + function generateRetailerFilePickerPolicy(options) { + var id = getId(options); + var handle = options && options["handle"]; + var convert = options && options["convert"] + + var url = "/api/retailers/" + id + "/filepicker_policy"; + + return $.ajax(url, { + data : { handle : handle, convert: convert }, + dataType : 'json' + }); + } + + function listRetailerInvitations(options) { + + var id = getId(options); + + return $.ajax({ + type: "GET", + url: "/api/retailers/" + id + '/invitations?' + $.param(options) , + dataType: "json", + contentType: 'application/json' + }); + } + + function createRetailerInvitation(options) { + + var id = getId(options); + + return $.ajax({ + type: "POST", + url: "/api/retailers/" + id + '/invitations?' + $.param(options) , + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }); + } + + function deleteRetailerInvitation(options) { + + var id = getId(options); + + return $.ajax({ + type: "DELETE", + url: "/api/retailers/" + id + '/invitations/' + options.invitation_id, + dataType: "json", + contentType: 'application/json' + }); + } + + function resendRetailerInvitation(options) { + + var id = getId(options); + + return $.ajax({ + type: "POST", + url: "/api/retaiers/" + id + '/invitations/' + options.invitation_id + '/resend', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }); + } + + function deleteRetailerStudent(options) { + + var id = getId(options); + + return $.ajax({ + type: "DELETE", + url: "/api/retailers/" + id + '/students/' + options.student_id, + dataType: "json", + contentType: 'application/json' + }); + } + + function deleteRetailerTeacher(options) { + + var id = getId(options); + + return $.ajax({ + type: "DELETE", + url: "/api/retailers/" + id + '/teachers/' + options.teacher_id, + dataType: "json", + contentType: 'application/json' + }); + } + function listTeacherDistributions(options) { if(!options) { @@ -2502,18 +2665,6 @@ }); } - function deleteSchoolTeacher(options) { - - var id = getId(options); - - return $.ajax({ - type: "DELETE", - url: "/api/schools/" + id + '/teachers/' + options.teacher_id, - dataType: "json", - contentType: 'application/json' - }); - } - function createReview(options) { return $.ajax({ @@ -2545,6 +2696,45 @@ }) } + function posaActivate(options) { + var slug = options.slug + delete options.slug + + return $.ajax({ + type: "POST", + url: '/api/posa/' + slug + '/activate', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options), + }) + } + + function posaClaim(options) { + + return $.ajax({ + type: "POST", + url: '/api/posa/claim', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options), + }) + } + + function sendRetailerCustomerEmail(options) { + options = options || {} + var retailerId = options.retailer + delete options.retailer + + return $.ajax({ + type: 'POST', + url: '/api/retailers/' + retailerId + '/customer_email', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function initialize() { return self; } @@ -2768,11 +2958,25 @@ this.resendSchoolInvitation = resendSchoolInvitation; this.deleteSchoolTeacher = deleteSchoolTeacher; this.deleteSchoolStudent = deleteSchoolStudent; + this.getRetailer = getRetailer; + this.updateRetailer = updateRetailer; + this.updateRetailerAvatar = updateRetailerAvatar; + this.deleteRetailerAvatar = deleteRetailerAvatar; + this.generateRetailerFilePickerPolicy = generateRetailerFilePickerPolicy; + this.listRetailerInvitations = listRetailerInvitations; + this.createRetailerInvitation = createRetailerInvitation; + this.deleteRetailerInvitation = deleteRetailerInvitation; + this.resendRetailerInvitation = resendRetailerInvitation; + this.deleteRetailerTeacher = deleteRetailerTeacher; + this.deleteRetailerStudent = deleteRetailerStudent; this.listTeacherDistributions = listTeacherDistributions; this.lessonStartTime = lessonStartTime; this.createReview = createReview; this.askSearchHelp = askSearchHelp; this.ratingDecision = ratingDecision; + this.posaActivate = posaActivate; + this.posaClaim = posaClaim; + this.sendRetailerCustomerEmail = sendRetailerCustomerEmail; return this; }; })(window,jQuery); diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index 10a21bb99..8a50c5178 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -8,6 +8,7 @@ //= require ./react-components/stores/UserActivityStore //= require ./react-components/stores/LessonTimerStore //= require ./react-components/stores/SchoolStore +//= require ./react-components/stores/RetailerStore //= require ./react-components/stores/JamBlasterStore //= require ./react-components/stores/StripeStore //= require ./react-components/stores/AvatarStore diff --git a/web/app/assets/javascripts/react-components/AccountRetailerScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountRetailerScreen.js.jsx.coffee new file mode 100644 index 000000000..887134368 --- /dev/null +++ b/web/app/assets/javascripts/react-components/AccountRetailerScreen.js.jsx.coffee @@ -0,0 +1,390 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +AppStore = context.AppStore +SchoolActions = context.RetailerActions +SchoolStore = context.RetailerStore +UserStore = context.UserStore + +profileUtils = context.JK.ProfileUtils + +@AccountRetailerScreen = React.createClass({ + + mixins: [ + ICheckMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(RetailerStore, "onRetailerChanged") + Reflux.listenTo(UserStore, "onUserChanged") + ] + + shownOnce: false + screenVisible: false + + TILE_ACCOUNT: 'account' + TILE_TEACHERS: 'teachers' + TILE_SALES: 'sales' + TILE_AGREEMENT: 'agreement' + + TILES: ['account', 'teachers', 'sales', 'agreement'] + + onAppInit: (@app) -> + @app.bindScreen('account/retailer', {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onSchoolChanged: (retailerState) -> + @setState(retailerState) + + onUserChanged: (userState) -> + @noRetailerCheck(userState?.user) + @setState({user: userState?.user}) + + componentDidMount: () -> + @checkboxes = [{selector: 'input.slot-decision', stateKey: 'slot-decision'}] + @root = $(@getDOMNode()) + @iCheckify() + + componentDidUpdate: () -> + @iCheckify() + + checkboxChanged: (e) -> + checked = $(e.target).is(':checked') + + value = $(e.target).val() + + #@setState({userSchedulingComm: value}) + + + beforeHide: (e) -> + #ProfileActions.viewTeacherProfileDone() + @screenVisible = false + + beforeShow: (e) -> + + noRetailerCheck: (user) -> + if user?.id? && @screenVisible + + if !user.owned_retailer_id? + window.JK.Banner.showAlert("You are not the owner of a retailer in our systems. If you are, please contact support@jamkazam.com and we'll update your account.") + return false + else + if !@shownOnce + @shownOnce = true + RetailerActions.refresh(user.owned_retailer_id) + + return true + + else + return false + + afterShow: (e) -> + @screenVisible = true + logger.debug("AccountRetailerScreen: afterShow") + logger.debug("after show", @state.user) + @noRetailerCheck(@state.user) + + getInitialState: () -> + { + retailer: null, + user: null, + selected: 'account', + updateErrors: null, + retailerName: null, + teacherInvitations: null, + updating: false + } + + nameValue: () -> + if this.state.retailerName? + this.state.retailerName + else + this.state.retailer.name + + nameChanged: (e) -> + $target = $(e.target) + val = $target.val() + @setState({retailerName: val}) + + onCancel: (e) -> + e.preventDefault() + context.location.href = '/client#/account' + + onUpdate: (e) -> + e.preventDefault() + + if this.state.updating + return + name = @root.find('input[name="name"]').val() + + @setState(updating: true) + rest.updateRetailer({ + id: this.state.retailer.id, + name: name, + }).done((response) => @onUpdateDone(response)).fail((jqXHR) => @onUpdateFail(jqXHR)) + + onUpdateDone: (response) -> + @setState({retailer: response, retailerName: null, updateErrors: null, updating: false}) + + @app.layout.notify({title: "update success", text: "Your retailer information has been successfully updated"}) + + onUpdateFail: (jqXHR) -> + handled = false + + @setState({updating: false}) + + if jqXHR.status == 422 + errors = JSON.parse(jqXHR.responseText) + handled = true + @setState({updateErrors: errors}) + + + if !handled + @app.ajaxError(jqXHR, null, null) + + inviteTeacher: () -> + @app.layout.showDialog('invite-retailer-user', {d1: true}) + + resendInvitation: (id, e) -> + e.preventDefault() + rest.resendRetailerInvitation({ + id: this.state.retailer.id, invitation_id: id + }).done((response) => @resendInvitationDone(response)).fail((jqXHR) => @resendInvitationFail(jqXHR)) + + resendInvitationDone: (response) -> + @app.layout.notify({title: 'invitation resent', text: 'Invitation was resent to ' + response.email}) + + resendInvitationFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + deleteInvitation: (id, e) -> + e.preventDefault() + rest.deleteRetailerInvitation({ + id: this.state.retailer.id, invitation_id: id + }).done((response) => @deleteInvitationDone(id, response)).fail((jqXHR) => @deleteInvitationFail(jqXHR)) + + deleteInvitationDone: (id, response) -> + context.RetailerActions.deleteInvitation(id) + + deleteInvitationFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + removeFromRetailer: (id, isTeacher, e) -> + if isTeacher + rest.deleteRetailerTeacher({id: this.state.retailer.id, teacher_id: id}).done((response) => @removeFromRetailerDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) + + removeFromRetailerDone: (retailer) -> + context.JK.Banner.showNotice("User removed", "User was removed from your retailer.") + context.RetailerActions.updateRetailer(retailer) + + removeFromSchoolFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + renderUser: (user, isTeacher) -> + photo_url = user.photo_url + if !photo_url? + photo_url = '/assets/shared/avatar_generic.png' + + `
+
+ +
+
+ {user.name} +
+
+ remove from retailer +
+
` + + + renderInvitation: (invitation) -> + `
+ + + + + +
{invitation.first_name} {invitation.last_name} +
has not yet accepted invitation
+ resend invitation + delete +
+ +
+
` + + renderTeachers: () -> + teachers = [] + + if this.state.retailer.teachers? && this.state.retailer.teachers.length > 0 + for teacher in this.state.retailer.teachers + teachers.push(@renderUser(teacher.user, true)) + else + teachers = `

No teachers

` + + teachers + + renderTeacherInvitations: () -> + invitations = [] + + if this.state.teacherInvitations? && this.state.teacherInvitations.length > 0 + for invitation in this.state.teacherInvitations + invitations.push(@renderInvitation(invitation)) + else + invitations = `

No pending invitations

` + invitations + + mainContent: () -> + if !@state.user? || !@state.retailer? + `
Loading...
` + else if @state.selected == @TILE_ACCOUNT + @account() + else if @state.selected == @TILE_TEACHERS + @teachers() + else if @state.selected == @TILE_SALES + @earnings() + else if @state.selected == @TILE_AGREEMENT + @agreement() + else + @account() + + account: () -> + ownerEmail = this.state.school.owner.email + correspondenceEmail = this.state.school.correspondence_email + correspondenceDisabled = !@isSchoolManaged() + + nameErrors = context.JK.reactSingleFieldErrors('name', @state.updateErrors) + correspondenceEmailErrors = context.JK.reactSingleFieldErrors('correspondence_email', @state.updateErrors) + nameClasses = classNames({name: true, error: nameErrors?, field: true}) + correspondenceEmailClasses = classNames({ + correspondence_email: true, + error: correspondenceEmailErrors?, + field: true + }) + + cancelClasses = { "button-grey": true, "cancel" : true, disabled: this.state.updating } + updateClasses = { "button-orange": true, "update" : true, disabled: this.state.updating } + + `
+
+ + + {nameErrors} +
+
+ + +
+ +

Management Preference

+ +
+
+ +
+
+ +
+
+
+ + + +
All emails relating to lesson scheduling will go to this email if school owner manages + scheduling. +
+ {correspondenceEmailErrors} +
+ +

Payments

+ +
+ +
+ +
+ CANCEL + UPDATE +
+
` + + + teachers: () -> + teachers = @renderTeachers() + teacherInvitations = @renderTeacherInvitations() + + `
+
+
+

teachers:

+ INVITE TEACHER +
+
+
+ {teacherInvitations} +
+ +
+ {teachers} +
+
+
` + + earnings: () -> + `
+

Coming soon

+
` + + agreement: () -> + `
+

The agreement between your music school and JamKazam is part of JamKazam's terms of service. You can find the + complete terms of service here. And you can find the section that is + most specific to the retailer terms here.

+
` + + selectionMade: (selection, e) -> + e.preventDefault() + @setState({selected: selection}) + + createTileLink: (i, tile) -> + active = this.state.selected == tile + classes = classNames({last: i == @TILES.length - 1, activeTile: active}) + + return `
{tile}
` + + onCustomBack: (customBack, e) -> + e.preventDefault() + context.location = customBack + + render: () -> + mainContent = @mainContent() + + profileSelections = [] + for tile, i in @TILES + profileSelections.push(@createTileLink(i, tile, profileSelections)) + + profileNav = `
+ {profileSelections} +
` + + `
+
+
retailer:
+ {profileNav} +
+
+ +
+
+
+ {mainContent} +
+
+
+
+
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee index 06753da19..b83d3a4c4 100644 --- a/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee @@ -44,9 +44,9 @@ context = window rest.createSchoolInvitation({id: school.id, as_teacher: this.state.teacher, email: email, last_name: lastName, first_name: firstName }).done((response) => @createDone(response)).fail((jqXHR) => @createFail(jqXHR)) createDone:(response) -> - context.SchoolActions.addInvitation(@state.teacher, response) + context.SchoolActions.addInvitation(response) context.JK.Banner.showNotice("invitation sent", "Your invitation has been sent!") - @app.layout.closeDialog('invite-school-user') + @app.layout.closeDialog('invite-retailer-user') createFail: (jqXHR) -> diff --git a/web/app/assets/javascripts/react-components/actions/RetailerActions.js.coffee b/web/app/assets/javascripts/react-components/actions/RetailerActions.js.coffee new file mode 100644 index 000000000..2a6b6d004 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/RetailerActions.js.coffee @@ -0,0 +1,9 @@ +context = window + +@RetailerActions = Reflux.createActions({ + refresh: {}, + addInvitation: {}, + deleteInvitation: {} + updateRetailer: {} +}) + diff --git a/web/app/assets/javascripts/react-components/landing/JamClassRetailerLandingBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassRetailerLandingBottomPage.js.jsx.coffee new file mode 100644 index 000000000..3cb0eb948 --- /dev/null +++ b/web/app/assets/javascripts/react-components/landing/JamClassRetailerLandingBottomPage.js.jsx.coffee @@ -0,0 +1,102 @@ +context = window +rest = context.JK.Rest() + +@JamClassRetailerLandingBottomPage = React.createClass({ + + render: () -> + `
+
+

How Our Retail Partner Program Can Help Your Store

+ +

By simply adding our free countertop display of music lesson and JamTracks gift cards to your store, you can + instantly be enabled to offer an amazing deal on music lessons to every customer who buys an instrument from + your store. Students can connect with amazing teachers anywhere in the country, avoid the time and hassle of + travel to/from lessons, and even record lessons to avoid forgetting what they’ve learned. Even if your store + offers lessons through in-store teachers, often your customers may live too far away or may not want to deal + with travel to get back to your store for lessons, and this is a great service you can offer, while earning + about $100 per year per student for students who stick with their lessons, in addition to 30% on the initial + gift card sale.

+ +

And for more advanced musicians who frequently wander into your store just to look around because they have + “the bug”, JamTracks are a terrific product you can sell to both beginner and advanced musicians. JamTracks + are full multitrack recordings of more than 4,000 popular songs. Your customers can solo a part they want to + play to hear all its nuances, mute that part out to play with the rest of the band, slow it down to practice, + record themselves playing along and share it with their friends on YouTube, and more. You’ll earn 30% margins + on JamTracks gift cards as well.

+ +

Watch the videos below that explain and show our JamClass online music lesson service and our JamTracks + products in more detail.

+ +
+

JamClass Kudos

+ +
+ + +

Julie Bonk

+ +
+ Oft-recorded pianist, teacher, mentor to Grammy winner Norah Jones and Scott Hoying of Pentatonix +
+
+
+ + + +

Carl Brown of GuitarLessions365

+
+
+ + +

Justin Pierce

+ +
+ Masters degree in jazz studies, performer in multiple bands, saxophone instructor +
+
+
+ + +

Dave Sebree

+ +
+ Founder of Austin School of Music, Gibson-endorsed guitarist, touring musician +
+
+
+ + +

Sara Nelson

+ +
+ Cellist for Austin Lyric Opera, frequently recorded with major artists +
+
+
+
+ +
+
+
+