From 2ec522a366499f5fcb68860f3a1eaa6cb25df278 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 9 Oct 2020 17:22:20 -0500 Subject: [PATCH] pause --- db/up/find_sessions_2020.sql | 25 ++++++++- ruby/lib/jam_ruby/models/sale.rb | 49 ++++++++++++++++- ruby/lib/jam_ruby/models/sale_line_item.rb | 27 +++++++++- ruby/lib/jam_ruby/recurly_client.rb | 38 +++++++++++++ .../Subscription.js.jsx.coffee | 54 +++++++++++++++---- .../AccountSubscriptionScreen.scss | 11 ++-- web/app/controllers/api_recurly_controller.rb | 51 ++++++++++++++++++ 7 files changed, 235 insertions(+), 20 deletions(-) diff --git a/db/up/find_sessions_2020.sql b/db/up/find_sessions_2020.sql index 0cb767776..f95fe61b2 100644 --- a/db/up/find_sessions_2020.sql +++ b/db/up/find_sessions_2020.sql @@ -56,4 +56,27 @@ ALTER TABLE users ADD COLUMN beta BOOLEAN default FALSE; ALTER TABLE arses ADD COLUMN beta BOOLEAN default FALSE; -ALTER TABLE generic_state ADD COLUMN event_page_top_logo_url VARCHAR(100000) DEFAULT '/assets/event/eventbrite-logo.png'; \ No newline at end of file +ALTER TABLE generic_state ADD COLUMN event_page_top_logo_url VARCHAR(100000) DEFAULT '/assets/event/eventbrite-logo.png'; + +ALTER TABLE users ADD COLUMN recurly_subscription_id VARCHAR(100) DEFAULT NULL; +ALTER TABLE users ADD COLUMN recurly_token VARCHAR(200) DEFAULT NULL; +ALTER TABLE users ADD COLUMN recurly_subscription_state VARCHAR(20) DEFAULT NULL; + + +CREATE TABLE subscriptions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(200) UNIQUE NOT NULL UNIQUE NOT NULL, + play_time_per_session_mins INT DEFAULT NULL, + play_time_per_month_mins INT DEFAULT NULL, + can_record BOOLEAN DEFAULT TRUE, + audio_max_bitrate INT DEFAULT NULL, + save_as_wave BOOLEAN DEFAULT FALSE, + pro_audio BOOLEAN DEFAULT FALSE, + video_resolution VARCHAR(50) DEFAULT NULL, + broadcasting_type VARCHAR(50) DEFAULT NULL, + music_lessons VARCHAR(50) DEFAULT NULL, + support VARCHAR(50) DEFAULT NULL, + max_players_per_session INT DEFAULT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index c71726434..3ef61a370 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -6,6 +6,7 @@ module JamRuby JAMTRACK_SALE = 'jamtrack' LESSON_SALE = 'lesson' POSA_SALE = 'posacard' + SUBSCRIPTION_SALE = 'subscription' SOURCE_RECURLY = 'recurly' SOURCE_IOS = 'ios' @@ -260,6 +261,41 @@ module JamRuby {sale: sale} end + def self.purchase_subscription(current_user, recurly_token, plan_code) + sale = nil + + Sale.transaction(:requires_new => true) do + + current_user.recurly_token = recurly_token + current_user.subscription_plan_code = plan_code + + sale = create_subscription_sale(current_user) + + if sale.valid? + + client = RecurlyClient.new + account = client.get_account(current_user) + + if account.present? + recurly_response = client.create_subscription(current_user, plan_code, account) + current_user.recurly_subscription_id = recurly_response.uuid + current_user.save(validate: false) + SaleLineItem.create_from_subscription(current_user, sale, plan_code, recurly_response) + + sale.recurly_subtotal_in_cents = recurly_response.unit_amount_in_cents + sale.recurly_tax_in_cents = recurly_response.tax_in_cents + sale.recurly_total_in_cents = sale.recurly_subtotal_in_cents + sale.recurly_tax_in_cents + sale.recurly_currency = recurly_response.currency + sale.save(validate: false) + else + raise RecurlyClientError, "Could not find account to place order." + end + 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, posa_card = nil) stripe_charge = nil @@ -776,7 +812,16 @@ module JamRuby def self.create_lesson_sale(user) sale = Sale.new sale.user = user - sale.sale_type = LESSON_SALE # gift cards and jam tracks are sold with this type of sale + sale.sale_type = LESSON_SALE + sale.order_total = 0 + sale.save + sale + end + + def self.create_subscription_sale(user) + sale = Sale.new + sale.user = user + sale.sale_type = SUBSCRIPTION_SALE sale.order_total = 0 sale.save sale @@ -785,7 +830,7 @@ module JamRuby 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.sale_type = POSA_SALE sale.order_total = posa_card.product_info[:price] sale.save sale diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index 188942ce7..2e6c7d203 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -7,6 +7,7 @@ module JamRuby GIFTCARD = 'GiftCardType' LESSON = 'LessonPackageType' POSACARD = 'PosaCard' + SUBSCRIPTION = 'Subscription' belongs_to :sale, class_name: 'JamRuby::Sale' belongs_to :jam_track, class_name: 'JamRuby::JamTrack' @@ -22,7 +23,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, POSACARD]} + validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD, LESSON, POSACARD, SUBSCRIPTION]} validates :unit_price, numericality: {only_integer: false} validates :quantity, numericality: {only_integer: true} validates :free, numericality: {only_integer: true} @@ -40,9 +41,15 @@ module JamRuby product_type == GIFTCARD end + def is_subscription? + product_type == SUBSCRIPTION + end + def product if product_type == JAMTRACK JamTrack.find_by_id(product_id) + if product_type == SUBSCRIPTION + {name: product_id} elsif product_type == GIFTCARD GiftCardType.find_by_id(product_id) elsif product_type == LESSON @@ -105,6 +112,24 @@ module JamRuby self.save! end end + + + def self.create_from_subscription(current_user, sale, plan_code, recurly_response) + sale_line_item = SaleLineItem.new + sale_line_item.product_type = SUBSCRIPTION + sale_line_item.product_id = recurly_response.uuid + sale_line_item.unit_price = recurly_response.unit_amount_in_cents + sale_line_item.quantity = 1 + sale_line_item.free = false + sale_line_item.sales_tax = recurly_response.tax_in_cents + sale_line_item.shipping_handling = 0 + sale_line_item.recurly_plan_code = plan_code + + sale.sale_line_items << sale_line_item + sale_line_item.save + sale_line_item + end + # in a shopping-cart less world (ios purchase), let's reuse as much logic as possible def self.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking) teacher = lesson_booking.teacher if lesson_booking diff --git a/ruby/lib/jam_ruby/recurly_client.rb b/ruby/lib/jam_ruby/recurly_client.rb index 23dff75c9..5530fe1a8 100644 --- a/ruby/lib/jam_ruby/recurly_client.rb +++ b/ruby/lib/jam_ruby/recurly_client.rb @@ -186,6 +186,44 @@ module JamRuby raise RecurlyClientError.new(plan.errors) if plan.errors.any? end + # https://dev.recurly.com/docs/create-subscription + def create_subscription(user, plan_code, account) + subscription = Recurly::Subscription.create( + :plan_code => plan_code, + :currency => 'USD', + :customer_notes => 'Thank you for your business!', + :account => { + :account_code => account.account_code + }, + :auto_renew => true + ) + subscription + end + + def find_subscription(user) + if user.recurly_subscription_id.nil? + nil + else + Recurly::Subscription.find(user.recurly_subscription_id) + end + end + + def sync_subscription(user) + subscription = find_subscription(user) + + if subscription.nil? + if user.subscription_plan_code + user.subscription_plan_code = nil + user.recurly_subscription_state = nil + user.save(validate:false) + end + else + user.recurly_subscription_state = subscription.state + if user.subscription_plan_code != subscription.plan.plan_code + user.subscription_plan_code = subscription.plan.plan_code + end + end + end def find_or_create_account(current_user, billing_info) account = get_account(current_user) diff --git a/web/app/assets/javascripts/react-components/Subscription.js.jsx.coffee b/web/app/assets/javascripts/react-components/Subscription.js.jsx.coffee index af2ee72a1..da7f8b60d 100644 --- a/web/app/assets/javascripts/react-components/Subscription.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/Subscription.js.jsx.coffee @@ -1,21 +1,35 @@ context = window rest = context.JK.Rest() logger = context.JK.logger - +LocationActions = context.LocationActions UserStore = context.UserStore @Subscription = React.createClass({ + mixins: [Reflux.listenTo(@LocationStore, "onLocationsChanged")] + + getInitialState: () -> { - clicked: false + clicked: false, + selectedCountry: null } + onLocationsChanged: (countries) -> + console.log("countires in ", countries) + @setState({countries: countries}) + + onCountryChanged: (e) -> + val = $(e.target).val() + @setState({selectedCountry: val}) + + currentCountry: () -> + this.state.selectedCountry || this.props.selectedCountry || '' + openBrowser: () -> context.JK.popExternalLink("https://www.jamkazam.com/client#/subscription") - onRecurlyToken: (err, token) -> console.log("TOKEN", token) if err @@ -38,6 +52,8 @@ UserStore = context.UserStore window.configuredRecurly = true componentDidMount: () -> + LocationActions.load() + @configureRecurly() @elements = recurly.Elements() @@ -67,32 +83,48 @@ UserStore = context.UserStore document.querySelector('#subscription-form').addEventListener('submit', @onFormSubmit.bind(this)) + defaultText: () -> + 'Select Country' + render: () -> + + if @state.countries? + countries = [``] + for countryId, countryInfo of @state.countries + countries.push(``) + + country = @state.countries[this.currentCountry()] + else + countries = [] + + countryJsx = ` + ` + `
- + - + - + - + - + - + - + - + {countryJsx}
diff --git a/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss b/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss index 1057c603e..0f949ee02 100644 --- a/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss +++ b/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss @@ -54,7 +54,7 @@ input[type="text"], select { position: relative; width: 100%; - border: 2px solid #c2c2c2; + border: 1px solid #c2c2c2; background: white; padding: 0.5rem; margin: 0 0 1rem; @@ -64,7 +64,7 @@ font-weight: bold; box-shadow: none; border-radius: 0; - color: #c2c2c2; + color: black; -webkit-appearance: none; -webkit-transition: border-color 0.3s; -moz-transition: border-color 0.3s; @@ -74,8 +74,9 @@ } input:focus { - border-color: #2c0730; - color: #2c0730; + border: 1px solid white; + color: black; + background:#d2d2d2; z-index: 10; } @@ -84,7 +85,7 @@ } div.error .recurly-hosted-field { - border: 2px solid #e43c29; + border: 1px solid #c2c2c2; } @media screen and (max-height: 599px) { diff --git a/web/app/controllers/api_recurly_controller.rb b/web/app/controllers/api_recurly_controller.rb index 1685a43e0..e3d31491a 100644 --- a/web/app/controllers/api_recurly_controller.rb +++ b/web/app/controllers/api_recurly_controller.rb @@ -124,6 +124,57 @@ class ApiRecurlyController < ApiController render json: {message: x.inspect, errors: x.errors}, :status => 404 end + def create_subscriptions + begin + sale = Sale.purchase_subscription(current_user, params[:recurly_token], params[:plan_code]) + subscription = Recurly::Subscription.find(current_user.recurly_subscription_id) + render :json => subscription.to_json + rescue RecurlyClientError => x + render json: {:message => x.inspect, errors: x.errors}, :status => 404 + end + end + + def get_subscription + subscription_id = current_user.recurly_subscription_id + subscription = Recurly::Subscription.find(subscription_id) if subscription_id + + if subscription + render :json => subscription + else + render :json => {} + end + end + + def cancel_subscription + begin + @client.cancel_subscription(current_user.recurly_subscription_id) + subscription = Recurly::Subscription.find(current_user.recurly_subscription_id) + render :json => subscription.to_json + rescue RecurlyClientError => x + render json: {:message => x.inspect, errors: x.errors}, :status => 404 + end + end + + def change_subscription_plan + begin + @client.change_subscription_plan(current_user.recurly_subscription_id, params[:plan_code]) + subscription = Recurly::Subscription.find(current_user.recurly_subscription_id) + render :json => subscription.to_json + rescue RecurlyClientError => x + render json: {:message => x.inspect, errors: x.errors}, :status => 404 + end + end + + def change_subscription_payment + begin + @client.change_subscription_payment(current_user.recurly_subscription_id, params[:recurly_token], params[:billing_ifo]) + subscription = Recurly::Subscription.find(current_user.recurly_subscription_id) + render :json => subscription.to_json + rescue RecurlyClientError => x + render json: {:message => x.inspect, errors: x.errors}, :status => 404 + end + end + def place_order error=nil response = {jam_tracks: [], gift_cards: []}