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 = ` + ` + `