This commit is contained in:
Seth Call 2015-06-08 14:51:00 -05:00
commit 2864b751f7
534 changed files with 35932 additions and 3716 deletions

View File

@ -2,48 +2,47 @@ ActiveAdmin.register JamRuby::AffiliatePartner, :as => 'Affiliates' do
menu :label => 'Partners', :parent => 'Affiliates'
config.sort_order = 'created_at DESC'
config.sort_order = 'referral_user_count DESC'
config.batch_actions = false
# config.clear_action_items!
config.filters = false
form :partial => 'form'
scope("Active", default: true) { |scope| scope.where('partner_user_id IS NOT NULL') }
scope("Unpaid") { |partner| partner.unpaid }
index do
column 'User' do |oo| link_to(oo.partner_user.name, "http://www.jamkazam.com/client#/profile/#{oo.partner_user.id}", {:title => oo.partner_user.name}) end
column 'Email' do |oo| oo.partner_user.email end
# default_actions # use this for all view/edit/delete links
column 'User' do |oo| link_to(oo.partner_user.name, admin_user_path(oo.partner_user.id), {:title => oo.partner_user.name}) end
column 'Name' do |oo| oo.partner_name end
column 'Code' do |oo| oo.partner_code end
column 'Type' do |oo| oo.entity_type end
column 'Code' do |oo| oo.id end
column 'Referral Count' do |oo| oo.referral_user_count end
# column 'Referrals' do |oo| link_to('View', admin_referrals_path(AffiliatePartner::PARAM_REFERRAL => oo.id)) end
column 'Earnings' do |oo| sprintf("$%.2f", oo.cumulative_earnings_in_dollars) end
column 'Amount Owed' do |oo| sprintf("$%.2f", oo.due_amount_in_cents.to_f / 100.to_f) end
column 'Pay Actions' do |oo|
link_to('Mark Paid', mark_paid_admin_affiliate_path(oo.id), :confirm => "Mark this affiliate as PAID?") if oo.unpaid
end
default_actions
end
action_item :only => [:show] do
link_to("Mark Paid",
mark_paid_admin_affiliate_path(resource.id),
:confirm => "Mark this affiliate as PAID?") if resource.unpaid
end
member_action :mark_paid, :method => :get do
resource.mark_paid
redirect_to admin_affiliate_path(resource.id)
end
controller do
def show
redirect_to admin_referrals_path(AffiliatePartner::PARAM_REFERRAL => resource.id)
end
def create
obj = AffiliatePartner.create_with_params(params[:jam_ruby_affiliate_partner])
if obj.errors.present?
set_resource_ivar(obj)
render active_admin_template('new')
else
redirect_to admin_affiliates_path
end
end
def update
obj = resource
vals = params[:jam_ruby_affiliate_partner]
obj.partner_name = vals[:partner_name]
obj.user_email = vals[:user_email] if vals[:user_email].present?
obj.save!
set_resource_ivar(obj)
render active_admin_template('show')
end
end
end

View File

@ -4,8 +4,6 @@ ActiveAdmin.register_page "Fake Purchaser" do
page_action :bulk_jamtrack_purchase, :method => :post do
puts params.inspect
user_field = params[:jam_ruby_jam_track_right][:user]
if user_field.blank?

View File

@ -0,0 +1,69 @@
ActiveAdmin.register JamRuby::FraudAlert, :as => 'Fraud Alerts' do
menu :label => 'Fraud Alerts', :parent => 'JamTracks'
config.sort_order = 'created_at desc'
config.batch_actions = false
scope("Not Whitelisted", default:true) { |scope|
scope.joins('INNER JOIN "machine_fingerprints" ON "machine_fingerprints"."id" = "fraud_alerts"."machine_fingerprint_id" LEFT OUTER JOIN "fingerprint_whitelists" ON "fingerprint_whitelists"."fingerprint" = "machine_fingerprints"."fingerprint"').where('fingerprint_whitelists IS NULL')}
index do
default_actions
column :machine_fingerprint
column :user
column :created_at
column :resolved
column "" do |alert|
link_to 'Matching MAC', "fraud_alerts/#{alert.id}/same_fingerprints"
end
column "" do |alert|
link_to 'Matching MAC and IP Address', "fraud_alerts/#{alert.id}/same_fingerprints_and_ip"
end
column "" do |alert|
link_to 'Matching IP Address', "fraud_alerts/#{alert.id}/same_ip"
end
column "" do |alert|
link_to 'Resolve', "fraud_alerts/#{alert.id}/resolve"
end
column "" do |alert|
link_to 'Whitelist Similar', "fraud_alerts/#{alert.id}/whitelist"
end
end
member_action :same_fingerprints, :method => :get do
alert = FraudAlert.find(params[:id])
redirect_to admin_machine_fingerprints_path("q[fingerprint_equals]" => alert.machine_fingerprint.fingerprint, commit: 'Filter', order: 'created_at_desc')
end
member_action :same_fingerprints_and_ip, :method => :get do
alert = FraudAlert.find(params[:id])
redirect_to admin_machine_fingerprints_path("q[fingerprint_equals]" => alert.machine_fingerprint.fingerprint, "q[remote_ip_equals]" => alert.machine_fingerprint.remote_ip, commit: 'Filter', order: 'created_at_desc')
end
member_action :resolve, :method => :get do
alert = FraudAlert.find(params[:id])
alert.resolved = true
alert.save!
redirect_to admin_fraud_alerts_path, notice: "That fraud alert has been marked as resolved"
end
member_action :whitelist, :method => :get do
alert = FraudAlert.find(params[:id])
wl = FingerprintWhitelist.new
wl.fingerprint = alert.machine_fingerprint.fingerprint
success = wl.save
redirect_to admin_fraud_alerts_path, notice: success ? "Added #{alert.machine_fingerprint.fingerprint} to whitelist" : "Could not add #{alert.machine_fingerprint.fingerprint} to whiteliste"
end
end

View File

@ -32,6 +32,7 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do
row :birth_date
row :gender
row :email_confirmed
row :remember_token
row :image do user.photo_url ? image_tag(user.photo_url) : '' end
end
active_admin_comments

View File

@ -1,20 +1,29 @@
require 'jam_ruby/recurly_client'
ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do
menu :label => 'Purchased JamTracks', :parent => 'JamTracks'
menu :label => 'Purchased JamTracks', :parent => 'Purchases'
config.sort_order = 'updated_at DESC'
config.sort_order = 'created_at DESC'
config.batch_actions = false
#form :partial => 'form'
filter :user_id
filter :user_id,
:label => "USER ID", :required => false,
:wrapper_html => { :style => "list-style: none" }
filter :jam_track
index do
default_actions
column "Order" do |right|
link_to("Place", order_admin_jam_track_right_path(right)) + " | " +
link_to("Refund", refund_admin_jam_track_right_path(right))
end
#column "Order" do |right|
#link_to("Place", order_admin_jam_track_right_path(right)) + " | " +
# link_to("Refund", refund_admin_jam_track_right_path(right))
#end
column "Last Name" do |right|
right.user.last_name
@ -23,13 +32,15 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do
right.user.first_name
end
column "Jam Track" do |right|
link_to(right.jam_track.name, admin_jam_track_right_path(right.jam_track))
link_to(right.jam_track.name, admin_jam_track_path(right.jam_track))
# right.jam_track
end
column "Plan Code" do |right|
right.jam_track.plan_code
end
column "Redeemed" do |right|
right.redeemed ? 'Y' : 'N'
end
end
@ -42,6 +53,9 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do
f.actions
end
=begin
member_action :order, :method => :get do
right = JamTrackRight.where("id=?",params[:id]).first
user = right.user
@ -84,4 +98,5 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do
redirect_to admin_jam_track_rights_path, notice: "Issued full refund on #{right.jam_track} for #{right.user.to_s}"
end
end
=end
end

View File

@ -0,0 +1,8 @@
ActiveAdmin.register JamRuby::MachineExtra, :as => 'Machine Extra' do
menu :label => 'Machine Extra', :parent => 'JamTracks'
config.sort_order = 'created_at desc'
config.batch_actions = false
end

View File

@ -0,0 +1,22 @@
ActiveAdmin.register JamRuby::MachineFingerprint, :as => 'Machine Fingerprints' do
menu :label => 'Machine Fingerprints', :parent => 'JamTracks'
config.sort_order = 'created_at desc'
config.batch_actions = false
index do
column :user
column 'Hash' do |fp|
fp.fingerprint
end
column :remote_ip
column 'Detail' do |fp|
detail = fp.detail
if detail
detail.to_s
end
end
column :created_at
end
end

View File

@ -15,6 +15,7 @@ ActiveAdmin.register JamRuby::Mix, :as => 'Mixes' do
end
end
index :as => :block do |mix|
div :for => mix do
h3 "Mix (Users: #{mix.recording.users.map { |u| u.name }.join ','}) (When: #{mix.created_at.strftime('%b %d %Y, %H:%M')})"
@ -26,6 +27,16 @@ ActiveAdmin.register JamRuby::Mix, :as => 'Mixes' do
row :created_at do |mix| mix.created_at.strftime('%b %d %Y, %H:%M') end
row :s3_url do |mix| mix.sign_url end
row :manifest do |mix| mix.manifest end
row :local_manifest do |mix|
div class: 'local-manifest' do
mix.local_manifest.to_json
end
end
row :download_script do |mix|
div class: 'download-script' do
mix.download_script
end
end
row :completed do |mix| "#{mix.completed ? "finished" : "not finished"}" end
if mix.completed
row :completed_at do |mix| mix.completed_at.strftime('%b %d %Y, %H:%M') end
@ -39,6 +50,7 @@ ActiveAdmin.register JamRuby::Mix, :as => 'Mixes' do
end
end
end
end
end
end

View File

@ -0,0 +1,13 @@
ActiveAdmin.register JamRuby::MusicSessionComment, :as => 'Ratings' do
config.per_page = 150
config.clear_action_items!
config.sort_order = 'created_at_desc'
menu :parent => 'Sessions', :label => 'Ratings'
index do
column :comment
column :user
column :created_at
end
end

View File

@ -2,12 +2,9 @@ ActiveAdmin.register_page "Recurly Health" do
menu :parent => 'Misc'
content :title => "Recurly Transaction Totals" do
table_for Sale.check_integrity do
table_for Sale.check_integrity_of_jam_track_sales do
column "Total", :total
column "Unknown", :not_known
column "Successes", :succeeded
column "Failures", :failed
column "Refunds", :refunded
column "Voids", :voided
end
end

View File

@ -0,0 +1,40 @@
ActiveAdmin.register JamRuby::RecurlyTransactionWebHook, :as => 'RecurlyHooks' do
menu :label => 'Recurly Transaction Hooks', :parent => 'Purchases'
config.sort_order = 'created_at DESC'
config.batch_actions = false
actions :all, :except => [:destroy]
#form :partial => 'form'
filter :transaction_type, :as => :select, :collection => JamRuby::RecurlyTransactionWebHook::HOOK_TYPES
filter :user_id,
:label => "USER ID", :required => false,
:wrapper_html => { :style => "list-style: none" }
filter :invoice_id
form :partial => 'form'
index do
default_actions
column :transaction_type
column :transaction_at
column :amount_in_cents
column 'Transaction' do |hook| link_to('Go to Recurly', Rails.application.config.recurly_root_url + "/transactions/#{hook.recurly_transaction_id}") end
column 'Invoice' do |hook| link_to(hook.invoice_number, Rails.application.config.recurly_root_url + "/invoices/#{hook.invoice_number}") end
column :admin_description
column 'User' do |hook| link_to("#{hook.user.email} (#{hook.user.name})", admin_user_path(hook.user.id)) end
#column "Order" do |right|
#link_to("Place", order_admin_jam_track_right_path(right)) + " | " +
# link_to("Refund", refund_admin_jam_track_right_path(right))
#end
end
end

View File

@ -0,0 +1,8 @@
ActiveAdmin.register JamRuby::SessionInfoComment, :as => 'Comments' do
config.per_page = 50
config.clear_action_items!
config.sort_order = 'created_at_desc'
menu :parent => 'Sessions', :label => 'Comments'
end

View File

@ -0,0 +1,25 @@
ActiveAdmin.register_page "Test Jobs" do
menu :parent => 'Misc'
page_action :long_running, :method => :post do
puts params.inspect
time = params[:long_running][:time]
Resque.enqueue(LongRunning, time)
redirect_to admin_test_jobs_path, :notice => "Long Running job enqueued."
end
content do
semantic_form_for LongRunning.new, :url => admin_test_jobs_long_running_path, :builder => ActiveAdmin::FormBuilder do |f|
f.inputs "Queue a long running job" do
f.input :time
end
f.actions
end
end
end

View File

@ -22,4 +22,11 @@
}
}
}
}
.download-script, .local-manifest {
white-space:pre-wrap;
background-color:white;
border:1px solid gray;
padding:5px;
}

View File

@ -55,7 +55,8 @@ class Cohort < ActiveRecord::Base
end
def self.earliest_cohort
starting = User.where(admin: false).order(:created_at).first.created_at
user = User.where(admin: false).order(:created_at).first
starting = user.created_at if user
starting = EARLIEST_DATE if starting.nil? || starting < EARLIEST_DATE
starting # this is necessary to always return not null
end

View File

@ -1,13 +1,7 @@
<%= semantic_form_for([:admin, resource], :url => resource.new_record? ? admin_affiliates_path : "/admin/affiliates/#{resource.id}") do |f| %>
<%= f.semantic_errors *f.object.errors.keys %>
<%= f.inputs do %>
<%= f.input(:user_email, :input_html => {:maxlength => 255}) %>
<%= f.input(:partner_name, :input_html => {:maxlength => 128}) %>
<% if resource.new_record? %>
<%= f.input(:partner_code, :input_html => {:maxlength => 128}) %>
<% else %>
<%= f.input(:partner_code, :input_html => {:maxlength => 128, :readonly => 'readonly'}) %>
<% end %>
<% end %>
<%= f.actions %>
<% end %>

View File

@ -0,0 +1,6 @@
= semantic_form_for([:admin, resource], :html => {:multipart => true}, :url => resource.new_record? ? admin_recurly_transaction_web_hooks_path : "#{ENV['RAILS_RELATIVE_URL_ROOT']}/admin/recurly_hooks/#{resource.id}") do |f|
= f.semantic_errors *f.object.errors.keys
= f.inputs name: 'Recurly Web Hook fields' do
= f.input :admin_description, :input_html => { :rows=>1, :maxlength=>200, }, hint: "this will display on the user's payment history page"
= f.input :jam_track, collection: JamRuby::JamTrack.all, include_blank: true, hint: "Please indicate which JamTrack this refund for, if not set"
= f.actions

View File

@ -83,6 +83,7 @@ module JamAdmin
config.external_port = ENV['EXTERNAL_PORT'] || 3000
config.external_protocol = ENV['EXTERNAL_PROTOCOL'] || 'http://'
config.external_root_url = "#{config.external_protocol}#{config.external_hostname}#{(config.external_port == 80 || config.external_port == 443) ? '' : ':' + config.external_port.to_s}"
config.recurly_root_url = 'https://jamkazam-development.recurly.com'
# where is rabbitmq?
config.rabbitmq_host = "localhost"
@ -116,7 +117,7 @@ module JamAdmin
config.email_smtp_domain = 'www.jamkazam.com'
config.email_smtp_authentication = :plain
config.email_smtp_user_name = 'jamkazam'
config.email_smtp_password = 'jamjamblueberryjam'
config.email_smtp_password = 'snorkeltoesniffyfarce1'
config.email_smtp_starttls_auto = true
config.facebook_app_id = ENV['FACEBOOK_APP_ID'] || '468555793186398'

View File

@ -43,4 +43,7 @@ JamAdmin::Application.configure do
# Show the logging configuration on STDOUT
config.show_log_configuration = true
config.email_generic_from = 'nobody-dev@jamkazam.com'
config.email_alerts_alias = 'alerts-dev@jamkazam.com'
end

View File

@ -2,8 +2,6 @@ class JamRuby::JamTrackTrack
# add a custom validation
attr_accessor :preview_generate_error
validate :preview
def preview
@ -52,87 +50,4 @@ class JamRuby::JamTrackTrack
end
end
def generate_preview
begin
Dir.mktmpdir do |tmp_dir|
input = File.join(tmp_dir, 'in.ogg')
output = File.join(tmp_dir, 'out.ogg')
output_mp3 = File.join(tmp_dir, 'out.mp3')
start = self.preview_start_time.to_f / 1000
stop = start + 20
raise 'no track' unless self["url_44"]
s3_manager.download(self.url_by_sample_rate(44), input)
command = "sox \"#{input}\" \"#{output}\" trim #{sprintf("%.3f", start)} =#{sprintf("%.3f", stop)}"
@@log.debug("trimming using: " + command)
sox_output = `#{command}`
result_code = $?.to_i
if result_code != 0
@@log.debug("fail #{result_code}")
@preview_generate_error = "unable to execute cut command #{sox_output}"
else
# now create mp3 off of ogg preview
convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\""
@@log.debug("converting to mp3 using: " + convert_mp3_cmd)
convert_output = `#{convert_mp3_cmd}`
result_code = $?.to_i
if result_code != 0
@@log.debug("fail #{result_code}")
@preview_generate_error = "unable to execute mp3 convert command #{convert_output}"
else
ogg_digest = ::Digest::MD5.file(output)
mp3_digest = ::Digest::MD5.file(output_mp3)
self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest
self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest
@@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}")
s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest)
@@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}")
s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64)
self.skip_uploader = true
original_ogg_preview_url = self["preview_url"]
original_mp3_preview_url = self["preview_mp3_url"]
# and finally update the JamTrackTrack with the new info
self["preview_url"] = self.preview_filename(ogg_md5, 'ogg')
self["preview_length"] = File.new(output).size
# and finally update the JamTrackTrack with the new info
self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3')
self["preview_mp3_length"] = File.new(output_mp3).size
self.save!
# if all that worked, now delete old previews, if present
begin
s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"]
s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"]
rescue
puts "UNABLE TO CLEANUP OLD PREVIEW URL"
end
end
end
end
rescue Exception => e
@@log.error("error in sox command #{e.to_s}")
@preview_generate_error = e.to_s
end
end
end

View File

@ -274,4 +274,16 @@ recording_client_metadata.sql
preview_support_mp3.sql
jam_track_duration.sql
sales.sql
broadcast_notifications.sql
show_whats_next_count.sql
recurly_adjustments.sql
signup_hints.sql
packaging_notices.sql
first_played_jamtrack_at.sql
payment_history.sql
jam_track_right_private_key.sql
first_downloaded_jamtrack_at.sql
signing.sql
optimized_redeemption.sql
optimized_redemption_warn_mode.sql
affiliate_partners2.sql
broadcast_notifications.sql

View File

@ -0,0 +1,132 @@
CREATE TABLE affiliate_legalese (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
legalese TEXT,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE users DROP CONSTRAINT users_affiliate_referral_id_fkey;
ALTER TABLE users DROP COLUMN affiliate_referral_id;
DROP TABLE affiliate_partners;
CREATE TABLE affiliate_partners (
id INTEGER PRIMARY KEY,
partner_name VARCHAR(1000),
partner_user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL,
entity_type VARCHAR(64),
legalese_id VARCHAR(64),
signed_at TIMESTAMP,
last_paid_at TIMESTAMP,
address JSON NOT NULL DEFAULT '{}',
tax_identifier VARCHAR(1000),
referral_user_count INTEGER NOT NULL DEFAULT 0,
cumulative_earnings_in_cents INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE SEQUENCE partner_key_sequence;
ALTER SEQUENCE partner_key_sequence RESTART WITH 10000;
ALTER TABLE affiliate_partners ALTER COLUMN id SET DEFAULT nextval('partner_key_sequence');;
ALTER TABLE users ADD COLUMN affiliate_referral_id INTEGER REFERENCES affiliate_partners(id) ON DELETE SET NULL;
CREATE INDEX affiliate_partners_legalese_idx ON affiliate_partners(legalese_id);
CREATE UNLOGGED TABLE affiliate_referral_visits (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
affiliate_partner_id INTEGER NOT NULL,
ip_address VARCHAR NOT NULL,
visited_url VARCHAR,
referral_url VARCHAR,
first_visit BOOLEAN NOT NULL DEFAULT TRUE,
user_id VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX on affiliate_referral_visits (affiliate_partner_id, created_at);
CREATE TABLE affiliate_quarterly_payments (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
quarter INTEGER NOT NULL,
year INTEGER NOT NULL,
affiliate_partner_id INTEGER NOT NULL REFERENCES affiliate_partners(id),
due_amount_in_cents INTEGER NOT NULL DEFAULT 0,
paid BOOLEAN NOT NULL DEFAULT FALSE,
closed BOOLEAN NOT NULL DEFAULT FALSE,
jamtracks_sold INTEGER NOT NULL DEFAULT 0,
closed_at TIMESTAMP,
paid_at TIMESTAMP,
last_updated TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE affiliate_monthly_payments (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
month INTEGER NOT NULL,
year INTEGER NOT NULL,
affiliate_partner_id INTEGER NOT NULL REFERENCES affiliate_partners(id),
due_amount_in_cents INTEGER NOT NULL DEFAULT 0,
closed BOOLEAN NOT NULL DEFAULT FALSE,
jamtracks_sold INTEGER NOT NULL DEFAULT 0,
closed_at TIMESTAMP,
last_updated TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX ON affiliate_quarterly_payments (affiliate_partner_id, year, quarter);
CREATE UNIQUE INDEX ON affiliate_quarterly_payments (year, quarter, affiliate_partner_id);
CREATE UNIQUE INDEX ON affiliate_monthly_payments (year, month, affiliate_partner_id);
CREATE INDEX ON affiliate_monthly_payments (affiliate_partner_id, year, month);
ALTER TABLE sale_line_items ADD COLUMN affiliate_referral_id INTEGER REFERENCES affiliate_partners(id);
ALTER TABLE sale_line_items ADD COLUMN affiliate_referral_fee_in_cents INTEGER;
ALTER TABLE sale_line_items ADD COLUMN affiliate_refunded BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE sale_line_items ADD COLUMN affiliate_refunded_at TIMESTAMP;
ALTER TABLE generic_state ADD COLUMN affiliate_tallied_at TIMESTAMP;
CREATE TABLE affiliate_traffic_totals (
day DATE NOT NULL,
signups INTEGER NOT NULL DEFAULT 0,
visits INTEGER NOT NULL DEFAULT 0,
affiliate_partner_id INTEGER NOT NULL REFERENCES affiliate_partners(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX ON affiliate_traffic_totals (day, affiliate_partner_id);
CREATE INDEX ON affiliate_traffic_totals (affiliate_partner_id, day);
CREATE VIEW affiliate_payments AS
SELECT id AS monthly_id,
CAST(NULL as VARCHAR) AS quarterly_id,
affiliate_partner_id,
due_amount_in_cents,
jamtracks_sold,
created_at,
closed,
CAST(NULL AS BOOLEAN) AS paid,
year,
month as month,
CAST(NULL AS INTEGER) as quarter,
month as time_sort,
'monthly' AS payment_type
FROM affiliate_monthly_payments
UNION ALL
SELECT CAST(NULL as VARCHAR) AS monthly_id,
id AS quarterly_id,
affiliate_partner_id,
due_amount_in_cents,
jamtracks_sold,
created_at,
closed,
paid,
year,
CAST(NULL AS INTEGER) as month,
quarter,
(quarter * 3) + 3 as time_sort,
'quarterly' AS payment_type
FROM affiliate_quarterly_payments;

View File

@ -0,0 +1 @@
ALTER TABLE jam_track_rights ADD COLUMN first_downloaded_at TIMESTAMP;

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN first_played_jamtrack_at TIMESTAMP;

View File

@ -1 +1,9 @@
ALTER TABLE jam_tracks ADD COLUMN duration INTEGER;
DO $$
BEGIN
BEGIN
ALTER TABLE jam_tracks ADD COLUMN duration INTEGER;
EXCEPTION
WHEN duplicate_column THEN RAISE NOTICE 'column duration already exists in jam_tracks.';
END;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1,11 @@
ALTER TABLE jam_track_rights ADD COLUMN private_key_44 VARCHAR;
ALTER TABLE jam_track_rights ADD COLUMN private_key_48 VARCHAR;
ALTER TABLE jam_track_rights ADD COLUMN signed_48 BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE jam_track_rights ADD COLUMN signed_44 BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE jam_track_rights DROP COLUMN private_key;
ALTER TABLE jam_track_rights DROP COLUMN signed;
ALTER TABLE jam_track_rights ADD COLUMN signing_started_at_44 TIMESTAMP;
ALTER TABLE jam_track_rights ADD COLUMN signing_started_at_48 TIMESTAMP;
ALTER TABLE jam_track_rights DROP COLUMN signing_started_at;

View File

@ -0,0 +1,13 @@
CREATE TABLE machine_fingerprints (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
fingerprint VARCHAR(20000) NOT NULL UNIQUE,
when_taken VARCHAR NOT NULL,
print_type VARCHAR NOT NULL,
remote_ip VARCHAR(1000) NOT NULL,
jam_track_right_id BIGINT REFERENCES jam_track_rights(id) ON DELETE SET NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE jam_track_rights ADD COLUMN redeemed_and_fingerprinted BOOLEAN DEFAULT FALSE;

View File

@ -0,0 +1,42 @@
DROP TABLE machine_fingerprints;
CREATE TABLE machine_fingerprints (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
fingerprint VARCHAR(20000) NOT NULL,
when_taken VARCHAR NOT NULL,
print_type VARCHAR NOT NULL,
remote_ip VARCHAR(1000) NOT NULL,
jam_track_right_id BIGINT REFERENCES jam_track_rights(id) ON DELETE SET NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE machine_extras (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
machine_fingerprint_id VARCHAR(64) NOT NULL REFERENCES machine_fingerprints(id) ON DELETE CASCADE,
mac_address VARCHAR(100),
mac_name VARCHAR(255),
upstate BOOLEAN,
ipaddr_0 VARCHAR(200),
ipaddr_1 VARCHAR(200),
ipaddr_2 VARCHAR(200),
ipaddr_3 VARCHAR(200),
ipaddr_4 VARCHAR(200),
ipaddr_5 VARCHAR(200),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE fraud_alerts (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
machine_fingerprint_id VARCHAR(64) NOT NULL REFERENCES machine_fingerprints(id) ON DELETE CASCADE,
user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
resolved BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE fingerprint_whitelists (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
fingerprint VARCHAR(20000) UNIQUE NOT NULL
);
CREATE INDEX machine_fingerprints_index1 ON machine_fingerprints USING btree (fingerprint, user_id, remote_ip, created_at);

View File

@ -0,0 +1,3 @@
ALTER TABLE jam_track_rights ADD COLUMN packaging_steps INTEGER;
ALTER TABLE jam_track_rights ADD COLUMN current_packaging_step INTEGER;
ALTER TABLE jam_track_rights ADD COLUMN last_step_at TIMESTAMP;

17
db/up/payment_history.sql Normal file
View File

@ -0,0 +1,17 @@
ALTER TABLE recurly_transaction_web_hooks ADD COLUMN admin_description VARCHAR;
ALTER TABLE recurly_transaction_web_hooks ADD COLUMN jam_track_id VARCHAR(64) REFERENCES jam_tracks(id);
CREATE VIEW payment_histories AS
SELECT id AS sale_id,
CAST(NULL as VARCHAR) AS recurly_transaction_web_hook_id,
user_id,
created_at,
'sale' AS transaction_type
FROM sales s
UNION ALL
SELECT CAST(NULL as VARCHAR) AS sale_id,
id AS recurly_transaction_web_hook_id,
user_id,
transaction_at AS created_at,
transaction_type
FROM recurly_transaction_web_hooks;

View File

@ -1,4 +1,11 @@
ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_url VARCHAR;
ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_md5 VARCHAR;
ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_length BIGINT;
UPDATE jam_track_tracks SET preview_url = NULL where track_type = 'Master';
DO $$
BEGIN
BEGIN
ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_url VARCHAR;
ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_md5 VARCHAR;
ALTER TABLE jam_track_tracks ADD COLUMN preview_mp3_length BIGINT;
EXCEPTION
WHEN duplicate_column THEN RAISE NOTICE 'preview mp3 columns already exist in jam_tracks';
END;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1,24 @@
ALTER TABLE sale_line_items ADD COLUMN recurly_adjustment_uuid VARCHAR(500);
ALTER TABLE sale_line_items ADD COLUMN recurly_adjustment_credit_uuid VARCHAR(500);
ALTER TABLE jam_track_rights ADD COLUMN recurly_adjustment_uuid VARCHAR(500);
ALTER TABLE jam_track_rights ADD COLUMN recurly_adjustment_credit_uuid VARCHAR(500);
ALTER TABLE sales ADD COLUMN recurly_invoice_id VARCHAR(500) UNIQUE;
ALTER TABLE sales ADD COLUMN recurly_invoice_number INTEGER;
ALTER TABLE sales ADD COLUMN recurly_subtotal_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN recurly_tax_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN recurly_total_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN recurly_currency VARCHAR;
ALTER TABLE sale_line_items ADD COLUMN recurly_tax_in_cents INTEGER;
ALTER TABLE sale_line_items ADD COLUMN recurly_total_in_cents INTEGER;
ALTER TABLE sale_line_items ADD COLUMN recurly_currency VARCHAR;
ALTER TABLE sale_line_items ADD COLUMN recurly_discount_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN sale_type VARCHAR NOT NULL DEFAULT 'jamtrack';
ALTER TABLE recurly_transaction_web_hooks ALTER COLUMN subscription_id DROP NOT NULL;
CREATE INDEX recurly_transaction_web_hooks_invoice_id_ndx ON recurly_transaction_web_hooks(invoice_id);
ALTER TABLE jam_track_rights DROP COLUMN recurly_subscription_uuid;

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN show_whats_next_count INTEGER NOT NULL DEFAULT 0;

2
db/up/signing.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE jam_track_rights ADD COLUMN signing_44 BOOLEAN DEFAULT FALSE;
ALTER TABLE jam_track_rights ADD COLUMN signing_48 BOOLEAN DEFAULT FALSE;

12
db/up/signup_hints.sql Normal file
View File

@ -0,0 +1,12 @@
CREATE TABLE signup_hints (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
anonymous_user_id VARCHAR(64) UNIQUE,
redirect_location VARCHAR,
want_jamblaster BOOLEAN NOT NULL DEFAULT FALSE,
user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE users ADD COLUMN want_jamblaster BOOLEAN NOT NULL DEFAULT FALSE;

View File

@ -20,11 +20,12 @@ require 'resque_mailer'
require 'rest-client'
require 'zip'
require 'csv'
require 'tzinfo'
require "jam_ruby/constants/limits"
require "jam_ruby/constants/notification_types"
require "jam_ruby/constants/validation_messages"
require "jam_ruby/errors/permission_error"
require "jam_ruby/errors/jam_permission_error"
require "jam_ruby/errors/state_error"
require "jam_ruby/errors/jam_argument_error"
require "jam_ruby/errors/conflict_error"
@ -58,10 +59,12 @@ require "jam_ruby/resque/scheduled/score_history_sweeper"
require "jam_ruby/resque/scheduled/scheduled_music_session_cleaner"
require "jam_ruby/resque/scheduled/recordings_cleaner"
require "jam_ruby/resque/scheduled/jam_tracks_cleaner"
require "jam_ruby/resque/jam_tracks_builder"
require "jam_ruby/resque/scheduled/stats_maker"
require "jam_ruby/resque/scheduled/tally_affiliates"
require "jam_ruby/resque/jam_tracks_builder"
require "jam_ruby/resque/google_analytics_event"
require "jam_ruby/resque/batch_email_job"
require "jam_ruby/resque/long_running"
require "jam_ruby/mq_router"
require "jam_ruby/recurly_client"
require "jam_ruby/base_manager"
@ -69,6 +72,7 @@ require "jam_ruby/connection_manager"
require "jam_ruby/version"
require "jam_ruby/environment"
require "jam_ruby/init"
require "jam_ruby/app/mailers/admin_mailer"
require "jam_ruby/app/mailers/user_mailer"
require "jam_ruby/app/mailers/invited_user_mailer"
require "jam_ruby/app/mailers/corp_mailer"
@ -99,6 +103,11 @@ require "jam_ruby/models/genre_player"
require "jam_ruby/models/genre"
require "jam_ruby/models/user"
require "jam_ruby/models/anonymous_user"
require "jam_ruby/models/signup_hint"
require "jam_ruby/models/machine_fingerprint"
require "jam_ruby/models/machine_extra"
require "jam_ruby/models/fraud_alert"
require "jam_ruby/models/fingerprint_whitelist"
require "jam_ruby/models/rsvp_request"
require "jam_ruby/models/rsvp_slot"
require "jam_ruby/models/rsvp_request_rsvp_slot"
@ -198,12 +207,19 @@ require "jam_ruby/app/mailers/async_mailer"
require "jam_ruby/app/mailers/batch_mailer"
require "jam_ruby/app/mailers/progress_mailer"
require "jam_ruby/models/affiliate_partner"
require "jam_ruby/models/affiliate_legalese"
require "jam_ruby/models/affiliate_quarterly_payment"
require "jam_ruby/models/affiliate_monthly_payment"
require "jam_ruby/models/affiliate_traffic_total"
require "jam_ruby/models/affiliate_referral_visit"
require "jam_ruby/models/affiliate_payment"
require "jam_ruby/models/chat_message"
require "jam_ruby/models/shopping_cart"
require "jam_ruby/models/generic_state"
require "jam_ruby/models/score_history"
require "jam_ruby/models/jam_company"
require "jam_ruby/models/user_sync"
require "jam_ruby/models/payment_history"
require "jam_ruby/models/video_source"
require "jam_ruby/models/text_message"
require "jam_ruby/models/sale"

View File

@ -0,0 +1,37 @@
module JamRuby
# sends out a boring ale
class AdminMailer < ActionMailer::Base
include SendGrid
DEFAULT_SENDER = "JamKazam <noreply@jamkazam.com>"
default :from => DEFAULT_SENDER
sendgrid_category :use_subject_lines
#sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth)
sendgrid_unique_args :env => Environment.mode
def alerts(options)
mail(to: APP_CONFIG.email_alerts_alias,
from: APP_CONFIG.email_generic_from,
body: options[:body],
content_type: "text/plain",
subject: options[:subject])
end
def recurly_alerts(user, options)
body = options[:body]
body << "\n\n"
body << "User " << user.admin_url + "\n"
body << "User's JamTracks " << user.jam_track_rights_admin_url + "\n"
mail(to: APP_CONFIG.email_recurly_notice,
from: APP_CONFIG.email_generic_from,
body: body,
content_type: "text/plain",
subject: options[:subject])
end
end
end

View File

@ -52,7 +52,7 @@ module JamRuby
def generate_signup_url(invited_user)
invited_user.generate_signup_url
# "http://www.jamkazam.com/signup?invitation_code=#{invited_user.invitation_code}"
# "https://www.jamkazam.com/signup?invitation_code=#{invited_user.invitation_code}"
end
end
end

View File

@ -23,6 +23,6 @@ class JamTrackRightUploader < CarrierWave::Uploader::Base
end
def filename
"#{model.store_dir}/#{model.filename}" if model.id
"#{model.store_dir}/#{model.filename(mounted_as)}" if model.id
end
end

View File

@ -8,7 +8,7 @@
<br/>
<br/>
<p>
This email was received because someone left feedback at <a style="color: #ffcc00;" href="http://www.jamkazam.com/corp/contact">http://www.jamkazam.com/corp/contact</a>
This email was received because someone left feedback at <a style="color: #ffcc00;" href="https://www.jamkazam.com/corp/contact">http://www.jamkazam.com/corp/contact</a>
</p>
</body>
</html>

View File

@ -5,4 +5,4 @@ From <%= @email %>:
<%= @body %>
This email was received because someone left feedback at http://www.jamkazam.com/corp/contact
This email was received because someone left feedback at https://www.jamkazam.com/corp/contact

View File

@ -7,7 +7,7 @@
</p>
<p>
<a style="color: #ffcc00;" href="http://www.jamkazam.com/downloads">Go to Download Page</a>
<a style="color: #ffcc00;" href="https://www.jamkazam.com/downloads">Go to Download Page</a>
</p>
<p>

View File

@ -4,7 +4,7 @@ Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
We noticed that you have registered as a JamKazam musician, but you have not yet downloaded and started using the free JamKazam application. You can find other musicians and listen to sessions and recordings on our website, but you need the free JamKazam application to play with other musicians online. Please click the link below to go to the download page for the free JamKazam application, or visit our JamKazam support center so that we can help you get up and running.
Go to Download Page: http://www.jamkazam.com/downloads
Go to Download Page: https://www.jamkazam.com/downloads
Go to Support Center: https://jamkazam.desk.com

View File

@ -10,7 +10,7 @@
Its still very early in our companys development, so we dont have zillions of users online on our service yet. If you click Find Session, you will often not find a good session to join, both due to the number of musicians online at any given time, and also because you wont see private sessions where groups of musicians dont want to be interrupted in their sessions.
</p>
<p>If you are having trouble getting into sessions, wed suggest you click the Musicians tile on the home screen of the app or the website: <a style="color: #ffcc00;" href="http://www.jamkazam.com/client#/musicians">Go To Musicians Page</a>
<p>If you are having trouble getting into sessions, wed suggest you click the Musicians tile on the home screen of the app or the website: <a style="color: #ffcc00;" href="https://www.jamkazam.com/client#/musicians">Go To Musicians Page</a>
</p>
<p>This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session.

View File

@ -7,7 +7,7 @@ We noticed that you havent yet played in a JamKazam session with multiple mus
Find Other Musicians on JamKazam
Its still very early in our companys development, so we dont have zillions of users online on our service yet. If you click Find Session, you will often not find a good session to join, both due to the number of musicians online at any given time, and also because you wont see private sessions where groups of musicians dont want to be interrupted in their sessions.
If you are having trouble getting into sessions, wed suggest you click the Musicians tile on the home screen of the app or the website: http://www.jamkazam.com/client#/musicians
If you are having trouble getting into sessions, wed suggest you click the Musicians tile on the home screen of the app or the website: https://www.jamkazam.com/client#/musicians
This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session.

View File

@ -7,7 +7,7 @@
</p>
<p>Find Other Musicians on JamKazam<br />
To find and connect with other musicians who are already on JamKazam, wed suggest you click the Musicians tile on the home screen of the app or the website: <a style="color: #ffcc00;" href="http://www.jamkazam.com/client#/musicians">Go To Musicians Page</a>
To find and connect with other musicians who are already on JamKazam, wed suggest you click the Musicians tile on the home screen of the app or the website: <a style="color: #ffcc00;" href="https://www.jamkazam.com/client#/musicians">Go To Musicians Page</a>
</p>
<p>This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that you read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session.

View File

@ -5,7 +5,7 @@ Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
We noticed that you havent yet connected with any friends on JamKazam. Connecting with friends is the best way to help you get into sessions with other musicians on JamKazam. Here are a couple of good ways to connect with others.
Find Other Musicians on JamKazam
To find and connect with other musicians who are already on JamKazam, wed suggest you click the Musicians tile on the home screen of the app or the website: http://www.jamkazam.com/client#/musicians
To find and connect with other musicians who are already on JamKazam, wed suggest you click the Musicians tile on the home screen of the app or the website: https://www.jamkazam.com/client#/musicians
This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that you read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session.

View File

@ -7,7 +7,7 @@
</p>
<% [:twitter, :facebook, :google].each do |site| %>
<%= link_to(image_tag("http://www.jamkazam.com/assets/content/icon_#{site}.png", :style => "vertical-align:top"), "http://www.jamkazam.com/endorse/@USERID/#{site}?src=email") %>&nbsp;
<%= link_to(image_tag("https://www.jamkazam.com/assets/content/icon_#{site}.png", :style => "vertical-align:top"), "https://www.jamkazam.com/endorse/@USERID/#{site}?src=email") %>&nbsp;
<% end %>
<p>-- Team JamKazam

View File

@ -5,7 +5,7 @@ Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
JamKazam is a young company/service built through the sweat and commitment of a small group of music-loving techies. Please help us continue to grow the service and attract more musicians to play online by liking and/or following us on Facebook, Twitter, and Google+. Just click the icons below to give us little push, thanks!
<% [:twitter, :facebook, :google].each do |site| %>
http://www.jamkazam.com/endorse/@USERID/#{site}?src=email
https://www.jamkazam.com/endorse/@USERID/#{site}?src=email
<% end %>

View File

@ -24,7 +24,7 @@ Hi <%= @user.first_name %>,
<% end %>
</table>
</p>
<p>There are currently <%= @new_musicians.size%> musicians on JamKazam with low enough latency Internet connections to you to support a good online session. To see ALL the JamKazam musicians with whom you may want to connect and play, view our Musicians page at: <a style="color: #ffcc00;" href="http://www.jamkazam.com/client#/musicians">http://www.jamkazam.com/client#/musicians</a>.
<p>There are currently <%= @new_musicians.size%> musicians on JamKazam with low enough latency Internet connections to you to support a good online session. To see ALL the JamKazam musicians with whom you may want to connect and play, view our Musicians page at: <a style="color: #ffcc00;" href="https://www.jamkazam.com/client#/musicians">http://www.jamkazam.com/client#/musicians</a>.
</p>
<p>Best Regards,</p>

View File

@ -11,7 +11,7 @@ The following new musicians have joined JamKazam within the last week, and have
<%= user.biography %>
<% end %>
There are currently <%= @new_musicians.size%> musicians on JamKazam with low enough latency Internet connections to you to support a good online session. To see ALL the JamKazam musicians with whom you may want to connect and play, view our Musicians page at: http://www.jamkazam.com/client#/musicians.
There are currently <%= @new_musicians.size%> musicians on JamKazam with low enough latency Internet connections to you to support a good online session. To see ALL the JamKazam musicians with whom you may want to connect and play, view our Musicians page at: https://www.jamkazam.com/client#/musicians.
Best Regards,
Team JamKazam

View File

@ -68,16 +68,16 @@
<td><%= sess.genre.description %></td>
<td>
<%= sess.name %><br/>
<a style="color: #ffcc00;" href="<%= "http://www.jamkazam.com/sessions/#{sess.id}/details" %>">Details</a>
<a style="color: #ffcc00;" href="<%= "https://www.jamkazam.com/sessions/#{sess.id}/details" %>">Details</a>
</td>
<td><%= sess.description %></td>
<td style="text-align:center">
<span class="latency">
<span class="latency-value"><%= (sess.latency / 2).round %> ms</span>
<% if sess.latency <= (APP_CONFIG.max_good_full_score / 2) %>
<%= image_tag("http://www.jamkazam.com/assets/content/icon_green_score.png", alt: 'good score icon') %>
<%= image_tag("https://www.jamkazam.com/assets/content/icon_green_score.png", alt: 'good score icon') %>
<% else %>
<%= image_tag("http://www.jamkazam.com/assets/content/icon_yellow_score.png", alt: 'fair score icon') %>
<%= image_tag("https://www.jamkazam.com/assets/content/icon_yellow_score.png", alt: 'fair score icon') %>
<% end %>
</span>
</td>
@ -86,7 +86,7 @@
</tbody>
</table>
<p>To see ALL the scheduled sessions that you might be interested in joining, view our <a style="color: #ffcc00;" href="http://www.jamkazam.com/client#/findSession">Find Session page</a>.</p>
<p>To see ALL the scheduled sessions that you might be interested in joining, view our <a style="color: #ffcc00;" href="https://www.jamkazam.com/client#/findSession">Find Session page</a>.</p>
<p>Best Regards,</p>

View File

@ -9,7 +9,7 @@ GENRE | NAME | DESCRIPTION | LATENCY
<%= sess.genre.description %> | <%= sess.name %> | <%= sess.description %> | <%= sess.latency %> ms
<% end %>
To see ALL the scheduled sessions that you might be interested in joining, view our Find Session page at: http://www.jamkazam.com/client#/findSession.
To see ALL the scheduled sessions that you might be interested in joining, view our Find Session page at: https://www.jamkazam.com/client#/findSession.
Best Regards,

View File

@ -19,7 +19,7 @@
<body bgcolor="#000000" style="margin-top:10px;font-family:Arial, Helvetica, sans-serif;">
<table bgcolor="#262626" width="650" align="center" cellpadding="0" cellspacing="0">
<tr>
<td><img src="http://www.jamkazam.com/assets/email/header.png" width="650" height="183" alt="JamKazam"></td>
<td><img src="https://www.jamkazam.com/assets/email/header.png" width="650" height="183" alt="JamKazam"></td>
</tr>
</table>
<table bgcolor="#262626" width="650" align="center" cellpadding="30" cellspacing="0">
@ -51,7 +51,7 @@
<!-- CALL OUT BOX -->
</font></p>
<p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a style="color: #ffcc00;" href="http://www.jamkazam.com">JamKazam</a>.
<p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a style="color: #ffcc00;" href="https://www.jamkazam.com">JamKazam</a>.
</td></tr></table>
</td>

View File

@ -2,7 +2,7 @@
<% unless @suppress_user_has_account_footer == true %>
This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com.
This email was sent to you because you have an account at JamKazam / https://www.jamkazam.com.
<% end %>
Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved.

View File

@ -39,7 +39,7 @@
<td align="left">
<!-- CALL OUT BOX -->
<p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a style="color: #ffcc00;" href="http://www.jamkazam.com">JamKazam</a>.&nbsp;&nbsp;Click <a style="color: #ffcc00;" href="http://www.jamkazam.com/client#/account/profile">here to unsubscribe</a> and update your profile settings.
<p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a style="color: #ffcc00;" href="http://www.jamkazam.com">JamKazam</a>.&nbsp;&nbsp;Click <a style="color: #ffcc00;" href="http://www.jamkazam.com/unsubscribe/#{@user.unsubscribe_token}">here to unsubscribe</a> and update your profile settings.
</font></p>
</td></tr></table>

View File

@ -4,8 +4,8 @@
<%= yield %>
<% end %>
<% unless @suppress_user_has_account_footer == true %>
This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com. Visit your profile page to unsubscribe: http://www.jamkazam.com/client#/account/profile.
<% unless @user.nil? || @suppress_user_has_account_footer == true %>
This email was sent to you because you have an account at JamKazam / https://www.jamkazam.com. To unsubscribe: https://www.jamkazam.com/unsubscribe/<%=@user.unsubscribe_token%>.
<% end %>
Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved.

View File

@ -0,0 +1,5 @@
module JamRuby
class JamPermissionError < Exception
end
end

View File

@ -1,5 +0,0 @@
module JamRuby
class PermissionError < Exception
end
end

View File

@ -91,13 +91,15 @@ module JamRuby
true
end
def synchronize_metadata(jam_track, metadata, metalocation, original_artist, name)
def synchronize_metadata(jam_track, metadata, metalocation, original_artist, name, options)
metadata ||= {}
self.name = metadata["name"] || name
if jam_track.new_record?
jam_track.id = "#{JamTrack.count + 1}" # default is UUID, but the initial import was based on auto-increment ID, so we'll maintain that
latest_jamtrack = JamTrack.order('created_at desc').first
id = latest_jamtrack.nil? ? 1 : latest_jamtrack.id.to_i + 1
jam_track.id = "#{id}" # default is UUID, but the initial import was based on auto-increment ID, so we'll maintain that
jam_track.status = 'Staging'
jam_track.metalocation = metalocation
jam_track.original_artist = metadata["original_artist"] || original_artist
@ -107,13 +109,20 @@ module JamRuby
jam_track.price = 1.99
jam_track.reproduction_royalty_amount = 0
jam_track.licensor_royalty_amount = 0
jam_track.sales_region = 'United States'
jam_track.sales_region = 'Worldwide'
jam_track.recording_type = 'Cover'
jam_track.description = "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the #{jam_track.original_artist} song \"#{jam_track.name}\"."
else
#@@log.debug("#{self.name} skipped because it already exists in database")
finish("jam_track_exists", "")
return false
if !options[:resync_audio]
#@@log.debug("#{self.name} skipped because it already exists in database")
finish("jam_track_exists", "")
return false
else
# jamtrack exists, leave it be
return true
end
end
saved = jam_track.save
@ -318,24 +327,31 @@ module JamRuby
def set_custom_weight(track)
weight = 5
case track.instrument_id
when 'electric guitar'
weight = 1
when 'acoustic guitar'
weight = 2
when 'drums'
weight = 3
when 'keys'
weight = 4
when 'computer'
weight = 10
else
weight = 5
end
if track.track_type == 'Master'
weight = 1000
# if there are any persisted tracks, do not sort from scratch; just stick new stuff at the end
if track.persisted?
weight = track.position
else
case track.instrument_id
when 'electric guitar'
weight = 100
when 'acoustic guitar'
weight = 200
when 'drums'
weight = 300
when 'keys'
weight = 400
when 'computer'
weight = 600
else
weight = 500
end
if track.track_type == 'Master'
weight = 1000
end
end
weight
end
@ -346,10 +362,19 @@ module JamRuby
a_weight <=> b_weight
end
# default to 1, but if there are any persisted tracks, this will get manipulated to be +1 the highest persisted track
position = 1
sorted_tracks.each do |track|
track.position = position
position = position + 1
if track.persisted?
# persisted tracks should be sorted at the beginning of the sorted_tracks,
# so this just keeps moving the 'position builder' up to +1 of the last persisted track
position = track.position + 1
else
track.position = position
position = position + 1
end
end
sorted_tracks[sorted_tracks.length - 1].position = 1000
@ -359,11 +384,40 @@ module JamRuby
def synchronize_audio(jam_track, metadata, s3_path, skip_audio_upload)
attempt_to_match_existing_tracks = true
# find all wav files in the JamTracks s3 bucket
wav_files = fetch_wav_files(s3_path)
tracks = []
wav_files.each do |wav_file|
if attempt_to_match_existing_tracks
# try to find a matching track from the JamTrack based on the name of the 44.1 path
basename = File.basename(wav_file)
ogg_44100_filename = File.basename(basename, ".wav") + "-44100.ogg"
found_track = nil
jam_track.jam_track_tracks.each do |jam_track_track|
if jam_track_track["url_44"] && jam_track_track["url_44"].end_with?(ogg_44100_filename)
# found a match!
found_track = jam_track_track
break
end
end
if found_track
@@log.debug("found a existing track to reuse")
found_track.original_audio_s3_path = wav_file
tracks << found_track
next
end
end
@@log.debug("no existing track found; creating a new one")
track = JamTrackTrack.new
track.original_audio_s3_path = wav_file
@ -388,6 +442,15 @@ module JamRuby
tracks << track
end
jam_track.jam_track_tracks.each do |jam_track_track|
# delete all jam_track_tracks not in the tracks array
unless tracks.include?(jam_track_track)
@@log.info("destroying removed JamTrackTrack #{jam_track_track.inspect}")
jam_track_track.destroy # should also delete s3 files associated with this jamtrack
end
end
@@log.info("sorting tracks")
tracks = sort_tracks(tracks)
jam_track.jam_track_tracks = tracks
@ -481,15 +544,16 @@ module JamRuby
track["length_48"] = File.new(ogg_48000).size
synchronize_duration(jam_track, ogg_44100)
jam_track.save!
# convert entire master ogg file to mp3, and push both to public destination
preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) if track.track_type == 'Master'
if track.track_type == 'Master'
preview_succeeded = synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_44100_digest)
if !preview_succeeded
return false
if !preview_succeeded
return false
end
end
end
track.save!
@ -596,7 +660,7 @@ module JamRuby
original_artist = parsed_metalocation[1]
name = parsed_metalocation[2]
success = synchronize_metadata(jam_track, metadata, metalocation, original_artist, name)
success = synchronize_metadata(jam_track, metadata, metalocation, original_artist, name, options)
return unless success
@ -614,7 +678,8 @@ module JamRuby
def synchronize_recurly(jam_track)
begin
recurly = RecurlyClient.new
recurly.create_jam_track_plan(jam_track) unless recurly.find_jam_track_plan(jam_track)
# no longer create JamTrack plans: VRFS-3028
# recurly.create_jam_track_plan(jam_track) unless recurly.find_jam_track_plan(jam_track)
rescue RecurlyClientError => x
finish('recurly_create_plan', x.errors.to_s)
return false
@ -654,6 +719,33 @@ module JamRuby
end
def synchronize_preview(jam_track)
importer = JamTrackImporter.new
importer.name = jam_track.name
error_occurred = false
error_msg = nil
jam_track.jam_track_tracks.each do |track|
next if track.track_type == 'Master'
if track.preview_start_time
track.generate_preview
if track.preview_generate_error
error_occurred = true
error_msg = track.preview_generate_error
else
end
end
end
if error_occurred
importer.finish('preview_error', error_msg)
else
importer.finish('success', nil)
end
importer
end
def synchronize_jamtrack_master_preview(jam_track)
importer = JamTrackImporter.new
importer.name = jam_track.name
@ -676,6 +768,32 @@ module JamRuby
importer
end
def synchronize_previews
importers = []
JamTrack.all.each do |jam_track|
importers << synchronize_preview(jam_track)
end
@@log.info("SUMMARY")
@@log.info("-------")
importers.each do |importer|
if importer
if importer.reason == "success" || importer.reason == "no_preview_start_time"
@@log.info("#{importer.name} #{importer.reason}")
else
@@log.error("#{importer.name} failed to import.")
@@log.error("#{importer.name} reason=#{importer.reason}")
@@log.error("#{importer.name} detail=#{importer.detail}")
end
else
@@log.error("NULL IMPORTER")
end
end
end
def synchronize_jamtrack_master_previews
importers = []

View File

@ -22,9 +22,21 @@ module JamRuby
save_jam_track_right_jkz(jam_track_right, sample_rate)
end
# increment the step, which causes a notification to be sent to the client so it can keep the UI fresh as the packaging step goes on
def bump_step(jam_track_right, step)
last_step_at = Time.now
jam_track_right.current_packaging_step = step
jam_track_right.last_step_at = Time.now
JamTrackRight.where(:id => jam_track_right.id).update_all(last_step_at: last_step_at, current_packaging_step: step)
SubscriptionMessage.jam_track_signing_job_change(jam_track_right)
step = step + 1
step
end
def save_jam_track_right_jkz(jam_track_right, sample_rate=48)
jam_track = jam_track_right.jam_track
py_root = APP_CONFIG.jamtracks_dir
step = 0
Dir.mktmpdir do |tmp_dir|
jam_file_opts=""
jam_track.jam_track_tracks.each do |jam_track_track|
@ -36,6 +48,9 @@ module JamRuby
track_filename = File.join(tmp_dir, nm)
track_url = jam_track_track.sign_url(120, sample_rate)
@@log.info("downloading #{track_url} to #{track_filename}")
step = bump_step(jam_track_right, step)
copy_url_to_file(track_url, track_filename)
jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{jam_track_track.part}")}"
end
@ -48,6 +63,8 @@ module JamRuby
version = jam_track.version
@@log.info "Executing python source in #{py_file}, outputting to #{tmp_dir} (#{output_jkz})"
step = bump_step(jam_track_right, step)
# From http://stackoverflow.com/questions/690151/getting-output-of-system-calls-in-ruby/5970819#5970819:
cli = "python #{py_file} -D -k #{sku} -p #{Shellwords.escape(tmp_dir)}/pkey.pem -s #{Shellwords.escape(tmp_dir)}/skey.pem #{jam_file_opts} -o #{Shellwords.escape(output_jkz)} -t #{Shellwords.escape(title)} -V #{Shellwords.escape(version)}"
Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr|
@ -65,10 +82,18 @@ module JamRuby
else
jam_track_right.url_44.store!(File.open(output_jkz, "rb"))
end
jam_track_right.signed=true
jam_track_right.downloaded_since_sign=false
jam_track_right.private_key=File.read("#{tmp_dir}/skey.pem")
if sample_rate == 48
jam_track_right.signed_48 = true
jam_track_right.private_key_48 = File.read("#{tmp_dir}/skey.pem")
else
jam_track_right.signed_44 = true
jam_track_right.private_key_44 = File.read("#{tmp_dir}/skey.pem")
end
jam_track_right.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts
jam_track_right.downloaded_since_sign = false
jam_track_right.save!
end
end # mktmpdir
@ -78,7 +103,7 @@ module JamRuby
def copy_url_to_file(url, filename)
uri = URI(url)
open(filename, 'w+b') do |io|
Net::HTTP.start(uri.host, uri.port) do |http|
Net::HTTP.start(uri.host, uri.port, use_ssl: url.start_with?('https') ? true : false) do |http|
request = Net::HTTP::Get.new uri
http.request request do |response|
response_code = response.code.to_i

View File

@ -22,7 +22,7 @@ module JamRuby
end
def self.jam_track_signing_job_change(jam_track_right)
Notification.send_subscription_message('jam_track_right', jam_track_right.id.to_s, {signing_state: jam_track_right.signing_state}.to_json )
Notification.send_subscription_message('jam_track_right', jam_track_right.id.to_s, {signing_state: jam_track_right.signing_state, current_packaging_step: jam_track_right.current_packaging_step, packaging_steps: jam_track_right.packaging_steps}.to_json )
end
end
end

View File

@ -7,7 +7,7 @@ module JamRuby
self.table_name = 'active_music_sessions'
attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording, :opening_backing_track, :opening_metronome
attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording, :opening_backing_track, :opening_metronome, :jam_track_id
belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions
belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id"
@ -774,6 +774,7 @@ module JamRuby
self.opening_jam_track = true
self.save
self.opening_jam_track = false
#self.tick_track_changes
end
def close_jam_track
@ -823,5 +824,19 @@ module JamRuby
music_session.band_id = session_history.band.id unless session_history.band.nil?
session_history.save!
end
def self.stats
stats = {}
result = ActiveMusicSession.select('count(distinct(id)) AS total, count(distinct(jam_track_initiator_id)) as jam_track_count, count(distinct(backing_track_initiator_id)) as backing_track_count, count(distinct(metronome_initiator_id)) as metronome_count, count(distinct(claimed_recording_initiator_id)) as recording_count').first
stats['count'] = result['total'].to_i
stats['jam_track_count'] = result['jam_track_count'].to_i
stats['backing_track_count'] = result['backing_track_count'].to_i
stats['metronome_count'] = result['metronome_count'].to_i
stats['recording_count'] = result['recording_count'].to_i
stats
end
end
end

View File

@ -0,0 +1,6 @@
class JamRuby::AffiliateLegalese < ActiveRecord::Base
self.table_name = 'affiliate_legalese'
has_many :affiliate_partners, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :legalese_id
end

View File

@ -0,0 +1,39 @@
class JamRuby::AffiliateMonthlyPayment < ActiveRecord::Base
belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :months
def self.index(user, options)
unless user.affiliate_partner
return [[], nil]
end
page = options[:page].to_i
per_page = options[:per_page].to_i
if page == 0
page = 1
end
if per_page == 0
per_page = 50
end
start = (page -1 ) * per_page
limit = per_page
query = AffiliateMonthlyPayment
.paginate(page: page, per_page: per_page)
.where(affiliate_partner_id: user.affiliate_partner.id)
.order('year ASC, month ASC')
if query.length == 0
[query, nil]
elsif query.length < limit
[query, nil]
else
[query, start + limit]
end
end
end

View File

@ -1,25 +1,75 @@
class JamRuby::AffiliatePartner < ActiveRecord::Base
belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id
has_many :user_referrals, :class_name => "JamRuby::User", :foreign_key => :affiliate_referral_id
self.table_name = 'affiliate_partners'
belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id, inverse_of: :affiliate_partner
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
has_many :quarters, :class_name => 'JamRuby::AffiliateQuarterlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
has_many :months, :class_name => 'JamRuby::AffiliateMonthlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
has_many :traffic_totals, :class_name => 'JamRuby::AffiliateTrafficTotal', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
has_many :visits, :class_name => 'JamRuby::AffiliateReferralVisit', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner
attr_accessible :partner_name, :partner_code, :partner_user_id
ENTITY_TYPES = %w{ Individual Sole\ Proprietor Limited\ Liability\ Company\ (LLC) Partnership Trust/Estate S\ Corporation C\ Corporation Other }
KEY_ADDR1 = 'address1'
KEY_ADDR2 = 'address2'
KEY_CITY = 'city'
KEY_STATE = 'state'
KEY_POSTAL = 'postal_code'
KEY_COUNTRY = 'country'
# ten dollars in cents
PAY_THRESHOLD = 10 * 100
AFFILIATE_PARAMS="utm_source=affiliate&utm_medium=affiliate&utm_campaign=2015-affiliate-custom&affiliate="
ADDRESS_SCHEMA = {
KEY_ADDR1 => '',
KEY_ADDR2 => '',
KEY_CITY => '',
KEY_STATE => '',
KEY_POSTAL => '',
KEY_COUNTRY => '',
}
PARAM_REFERRAL = :ref
PARAM_COOKIE = :affiliate_ref
PARTNER_CODE_REGEX = /^[#{Regexp.escape('abcdefghijklmnopqrstuvwxyz0123456789-._+,')}]+{2,128}$/i
validates :user_email, format: {with: JamRuby::User::VALID_EMAIL_REGEX}, :if => :user_email
validates :partner_name, presence: true
validates :partner_code, presence: true, format: { with: PARTNER_CODE_REGEX }
validates :partner_user, presence: true
#validates :user_email, format: {with: JamRuby::User::VALID_EMAIL_REGEX}, :if => :user_email
#validates :partner_code, format: { with: PARTNER_CODE_REGEX }, :allow_blank => true
validates :entity_type, inclusion: {in: ENTITY_TYPES, message: "invalid entity type"}
serialize :address, JSON
before_save do |record|
record.address ||= ADDRESS_SCHEMA.clone
record.entity_type ||= ENTITY_TYPES.first
end
# used by admin
def self.create_with_params(params={})
raise 'not supported'
oo = self.new
oo.partner_name = params[:partner_name].try(:strip)
oo.partner_code = params[:partner_code].try(:strip).try(:downcase)
oo.partner_user = User.where(:email => params[:user_email].try(:strip)).limit(1).first
oo.partner_user_id = oo.partner_user.try(:id)
oo.entity_type = params[:entity_type] || ENTITY_TYPES.first
oo.save
oo
end
# used by web
def self.create_with_web_params(user, params={})
oo = self.new
oo.partner_name = params[:partner_name].try(:strip)
oo.partner_user = user if user # user is not required
oo.entity_type = params[:entity_type] || ENTITY_TYPES.first
oo.signed_at = Time.now
oo.save
oo
end
@ -29,16 +79,394 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
end
def self.is_code?(code)
self.where(:partner_code => code).limit(1).pluck(:id).present?
self.where(:partner_code => code).limit(1).pluck(:id).present?
end
def referrals_by_date
by_date = User.where(:affiliate_referral_id => self.id)
.group('DATE(created_at)')
.having("COUNT(*) > 0")
.order('date_created_at DESC')
.count
.group('DATE(created_at)')
.having("COUNT(*) > 0")
.order('date_created_at DESC')
.count
block_given? ? yield(by_date) : by_date
end
def signed_legalese(legalese)
self.affiliate_legalese = legalese
self.signed_at = Time.now
save!
end
def update_address_value(key, val)
self.address[key] = val
self.update_attribute(:address, self.address)
end
def address_value(key)
self.address[key]
end
def created_within_affiliate_window(user, sale_time)
sale_time - user.created_at < 2.years
end
def should_attribute_sale?(shopping_cart)
if shopping_cart.is_jam_track?
if created_within_affiliate_window(shopping_cart.user, Time.now)
product_info = shopping_cart.product_info
# subtract the total quantity from the freebie quantity, to see how much we should attribute to them
real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i
{fee_in_cents: real_quantity * 20}
else
false
end
else
raise 'shopping cart type not implemented yet'
end
end
def cumulative_earnings_in_dollars
cumulative_earnings_in_cents.to_f / 100.to_f
end
def self.quarter_info(date)
year = date.year
# which quarter?
quarter = -1
if date.month >= 1 && date.month <= 3
quarter = 0
elsif date.month >= 4 && date.month <= 6
quarter = 1
elsif date.month >= 7 && date.month <= 9
quarter = 2
elsif date.month >= 10 && date.month <= 12
quarter = 3
end
raise 'quarter should never be -1' if quarter == -1
previous_quarter = quarter - 1
previous_year = date.year
if previous_quarter == -1
previous_quarter = 3
previous_year = year - 1
end
raise 'previous quarter should never be -1' if previous_quarter == -1
{year: year, quarter: quarter, previous_quarter: previous_quarter, previous_year: previous_year}
end
def self.did_quarter_elapse?(quarter_info, last_tallied_info)
if last_tallied_info.nil?
true
else
quarter_info == last_tallied_info
end
end
# meant to be run regularly; this routine will make summarized counts in the
# AffiliateQuarterlyPayment table
# AffiliatePartner.cumulative_earnings_in_cents, AffiliatePartner.referral_user_count
def self.tally_up(day)
AffiliatePartner.transaction do
quarter_info = quarter_info(day)
last_tallied_info = quarter_info(GenericState.affiliate_tallied_at) if GenericState.affiliate_tallied_at
quarter_elapsed = did_quarter_elapse?(quarter_info, last_tallied_info)
if quarter_elapsed
tally_monthly_payments(quarter_info[:previous_year], quarter_info[:previous_quarter])
tally_quarterly_payments(quarter_info[:previous_year], quarter_info[:previous_quarter])
end
tally_monthly_payments(quarter_info[:year], quarter_info[:quarter])
tally_quarterly_payments(quarter_info[:year], quarter_info[:quarter])
tally_traffic_totals(GenericState.affiliate_tallied_at, day)
tally_partner_totals
state = GenericState.singleton
state.affiliate_tallied_at = day
state.save!
end
end
# this just makes sure that the quarter rows exist before later manipulations with UPDATEs
def self.ensure_quarters_exist(year, quarter)
sql = %{
INSERT INTO affiliate_quarterly_payments (quarter, year, affiliate_partner_id)
(SELECT #{quarter}, #{year}, affiliate_partners.id FROM affiliate_partners WHERE affiliate_partners.partner_user_id IS NOT NULL AND affiliate_partners.id NOT IN
(SELECT affiliate_partner_id FROM affiliate_quarterly_payments WHERE year = #{year} AND quarter = #{quarter}))
}
ActiveRecord::Base.connection.execute(sql)
end
# this just makes sure that the quarter rows exist before later manipulations with UPDATEs
def self.ensure_months_exist(year, quarter)
months = [1, 2, 3].collect! { |i| quarter * 3 + i }
months.each do |month|
sql = %{
INSERT INTO affiliate_monthly_payments (month, year, affiliate_partner_id)
(SELECT #{month}, #{year}, affiliate_partners.id FROM affiliate_partners WHERE affiliate_partners.partner_user_id IS NOT NULL AND affiliate_partners.id NOT IN
(SELECT affiliate_partner_id FROM affiliate_monthly_payments WHERE year = #{year} AND month = #{month}))
}
ActiveRecord::Base.connection.execute(sql)
end
end
def self.sale_items_subquery(start_date, end_date, table_name)
%{
FROM sale_line_items
WHERE
(DATE(sale_line_items.created_at) >= DATE('#{start_date}') AND DATE(sale_line_items.created_at) <= DATE('#{end_date}'))
AND
sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id
}
end
def self.sale_items_refunded_subquery(start_date, end_date, table_name)
%{
FROM sale_line_items
WHERE
(DATE(sale_line_items.affiliate_refunded_at) >= DATE('#{start_date}') AND DATE(sale_line_items.affiliate_refunded_at) <= DATE('#{end_date}'))
AND
sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id
AND
sale_line_items.affiliate_refunded = TRUE
}
end
# total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id
# don't forget to substract any sale_line_items that have a affiliate_refunded = TRUE
def self.total_months(year, quarter)
months = [1, 2, 3].collect! { |i| quarter * 3 + i }
months.each do |month|
start_date, end_date = boundary_dates_for_month(year, month)
sql = %{
UPDATE affiliate_monthly_payments
SET
last_updated = NOW(),
jamtracks_sold =
COALESCE(
(SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
#{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')}
), 0)
+
COALESCE(
(SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
#{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')}
), 0),
due_amount_in_cents =
COALESCE(
(SELECT SUM(affiliate_referral_fee_in_cents)
#{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')}
), 0)
+
COALESCE(
(SELECT -SUM(affiliate_referral_fee_in_cents)
#{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')}
), 0)
WHERE closed = FALSE AND year = #{year} AND month = #{month}
}
ActiveRecord::Base.connection.execute(sql)
end
end
# close any quarters that are done, so we don't manipulate them again
def self.close_months(year, quarter)
# close any quarters that occurred before this quarter
month = quarter * 3 + 1
sql = %{
UPDATE affiliate_monthly_payments
SET
closed = TRUE, closed_at = NOW()
WHERE year < #{year} OR month < #{month}
}
ActiveRecord::Base.connection.execute(sql)
end
# total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id
# don't forget to substract any sale_line_items that have a affiliate_refunded = TRUE
def self.total_quarters(year, quarter)
start_date, end_date = boundary_dates(year, quarter)
sql = %{
UPDATE affiliate_quarterly_payments
SET
last_updated = NOW(),
jamtracks_sold =
COALESCE(
(SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
#{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
), 0)
+
COALESCE(
(SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END)
#{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
), 0),
due_amount_in_cents =
COALESCE(
(SELECT SUM(affiliate_referral_fee_in_cents)
#{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
), 0)
+
COALESCE(
(SELECT -SUM(affiliate_referral_fee_in_cents)
#{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')}
), 0)
WHERE closed = FALSE AND paid = FALSE AND year = #{year} AND quarter = #{quarter}
}
ActiveRecord::Base.connection.execute(sql)
end
# close any quarters that are done, so we don't manipulate them again
def self.close_quarters(year, quarter)
# close any quarters that occurred before this quarter
sql = %{
UPDATE affiliate_quarterly_payments
SET
closed = TRUE, closed_at = NOW()
WHERE year < #{year} OR quarter < #{quarter}
}
ActiveRecord::Base.connection.execute(sql)
end
def self.tally_quarterly_payments(year, quarter)
ensure_quarters_exist(year, quarter)
total_quarters(year, quarter)
close_quarters(year, quarter)
end
def self.tally_monthly_payments(year, quarter)
ensure_months_exist(year, quarter)
total_months(year, quarter)
close_months(year, quarter)
end
def self.tally_partner_totals
sql = %{
UPDATE affiliate_partners SET
referral_user_count = (SELECT count(*) FROM users WHERE affiliate_partners.id = users.affiliate_referral_id),
cumulative_earnings_in_cents = (SELECT COALESCE(SUM(due_amount_in_cents), 0) FROM affiliate_quarterly_payments AS aqp WHERE aqp.affiliate_partner_id = affiliate_partners.id AND closed = TRUE and paid = TRUE)
}
ActiveRecord::Base.connection.execute(sql)
end
def self.tally_traffic_totals(last_tallied_at, target_day)
if last_tallied_at
start_date = last_tallied_at.to_date
end_date = target_day.to_date
else
start_date = target_day.to_date - 1
end_date = target_day.to_date
end
if start_date == end_date
return
end
sql = %{
INSERT INTO affiliate_traffic_totals(SELECT day, 0, 0, ap.id FROM affiliate_partners AS ap CROSS JOIN (select (generate_series('#{start_date}', '#{end_date - 1}', '1 day'::interval))::date as day) AS lurp)
}
ActiveRecord::Base.connection.execute(sql)
sql = %{
UPDATE affiliate_traffic_totals traffic SET visits = COALESCE((SELECT COALESCE(count(affiliate_partner_id), 0) FROM affiliate_referral_visits v WHERE DATE(v.created_at) >= DATE('#{start_date}') AND DATE(v.created_at) < DATE('#{end_date}') AND v.created_at::date = traffic.day AND v.affiliate_partner_id = traffic.affiliate_partner_id GROUP BY affiliate_partner_id, v.created_at::date ), 0) WHERE traffic.day >= DATE('#{start_date}') AND traffic.day < DATE('#{end_date}')
}
ActiveRecord::Base.connection.execute(sql)
sql = %{
UPDATE affiliate_traffic_totals traffic SET signups = COALESCE((SELECT COALESCE(count(v.id), 0) FROM users v WHERE DATE(v.created_at) >= DATE('#{start_date}') AND DATE(v.created_at) < DATE('#{end_date}') AND v.created_at::date = traffic.day AND v.affiliate_referral_id = traffic.affiliate_partner_id GROUP BY affiliate_referral_id, v.created_at::date ), 0) WHERE traffic.day >= DATE('#{start_date}') AND traffic.day < DATE('#{end_date}')
}
ActiveRecord::Base.connection.execute(sql)
end
def self.boundary_dates(year, quarter)
if quarter == 0
[Date.new(year, 1, 1), Date.new(year, 3, 31)]
elsif quarter == 1
[Date.new(year, 4, 1), Date.new(year, 6, 30)]
elsif quarter == 2
[Date.new(year, 7, 1), Date.new(year, 9, 30)]
elsif quarter == 3
[Date.new(year, 10, 1), Date.new(year, 12, 31)]
else
raise "invalid quarter #{quarter}"
end
end
# 1-based month
def self.boundary_dates_for_month(year, month)
[Date.new(year, month, 1), Date.civil(year, month, -1)]
end
# Finds all affiliates that need to be paid
def self.unpaid
joins(:quarters)
.where('affiliate_quarterly_payments.paid = false').where('affiliate_quarterly_payments.closed = true')
.group('affiliate_partners.id')
.having('sum(due_amount_in_cents) >= ?', PAY_THRESHOLD)
.order('sum(due_amount_in_cents) DESC')
end
# does this one affiliate need to be paid?
def unpaid
due_amount_in_cents > PAY_THRESHOLD
end
# admin function: mark the affiliate paid
def mark_paid
if unpaid
transaction do
now = Time.now
quarters.where(paid:false, closed:true).update_all(paid:true, paid_at: now)
self.last_paid_at = now
self.save!
end
end
end
# how much is this affiliate due?
def due_amount_in_cents
total_in_cents = 0
quarters.where(paid:false, closed:true).each do |quarter|
total_in_cents = total_in_cents + quarter.due_amount_in_cents
end
total_in_cents
end
def affiliate_query_params
AffiliatePartner::AFFILIATE_PARAMS + self.id.to_s
end
end

View File

@ -0,0 +1,49 @@
module JamRuby
class AffiliatePayment < ActiveRecord::Base
belongs_to :affiliate_monthly_payment
belongs_to :affiliate_quarterly_payment
def self.index(user, options)
unless user.affiliate_partner
return [[], nil]
end
affiliate_partner_id = user.affiliate_partner.id
page = options[:page].to_i
per_page = options[:per_page].to_i
if page == 0
page = 1
end
if per_page == 0
per_page = 50
end
start = (page -1 ) * per_page
limit = per_page
query = AffiliatePayment
.includes(affiliate_quarterly_payment: [], affiliate_monthly_payment:[])
.where(affiliate_partner_id: affiliate_partner_id)
.where("(payment_type='quarterly' AND closed = true) OR payment_type='monthly'")
.where('(paid = TRUE or due_amount_in_cents < 10000 or paid is NULL)')
.paginate(:page => page, :per_page => limit)
.order('year ASC, time_sort ASC, payment_type ASC')
if query.length == 0
[query, nil]
elsif query.length < limit
[query, nil]
else
[query, start + limit]
end
end
end
end

View File

@ -0,0 +1,41 @@
class JamRuby::AffiliateQuarterlyPayment < ActiveRecord::Base
belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :quarters
def self.index(user, options)
unless user.affiliate_partner
return [[], nil]
end
page = options[:page].to_i
per_page = options[:per_page].to_i
if page == 0
page = 1
end
if per_page == 0
per_page = 50
end
start = (page -1 ) * per_page
limit = per_page
query = AffiliateQuarterlyPayment
.paginate(page: page, per_page: per_page)
.where(affiliate_partner_id: user.affiliate_partner.id)
.where(closed:true)
.where(paid:true)
.order('year ASC, quarter ASC')
if query.length == 0
[query, nil]
elsif query.length < limit
[query, nil]
else
[query, start + limit]
end
end
end

View File

@ -0,0 +1,23 @@
class JamRuby::AffiliateReferralVisit < ActiveRecord::Base
belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :visits
validates :affiliate_partner_id, numericality: {only_integer: true}, :allow_nil => true
validates :visited_url, length: {maximum: 1000}
validates :referral_url, length: {maximum: 1000}
validates :ip_address, presence: true, length: {maximum:1000}
validates :first_visit, inclusion: {in: [true, false]}
validates :user_id, length: {maximum:64}
def self.track(options = {})
visit = AffiliateReferralVisit.new
visit.affiliate_partner_id = options[:affiliate_id]
visit.ip_address = options[:remote_ip]
visit.visited_url = options[:visited_url]
visit.referral_url = options[:referral_url]
visit.first_visit = options[:visited].nil?
visit.user_id = options[:current_user].id if options[:current_user]
visit.save
visit
end
end

View File

@ -0,0 +1,39 @@
class JamRuby::AffiliateTrafficTotal < ActiveRecord::Base
belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :traffic_totals
def self.index(user, options)
unless user.affiliate_partner
return [[], nil]
end
page = options[:page].to_i
per_page = options[:per_page].to_i
if page == 0
page = 1
end
if per_page == 0
per_page = 50
end
start = (page -1 ) * per_page
limit = per_page
query = AffiliateTrafficTotal
.paginate(page: page, per_page: per_page)
.where(affiliate_partner_id: user.affiliate_partner.id)
.where('visits != 0 OR signups != 0')
.order('day ASC')
if query.length == 0
[query, nil]
elsif query.length < limit
[query, nil]
else
[query, start + limit]
end
end
end

View File

@ -4,14 +4,15 @@
module JamRuby
class AnonymousUser
attr_accessor :id
attr_accessor :id, :cookies
def initialize(id)
def initialize(id, cookies)
@id = id
@cookies = cookies
end
def shopping_carts
ShoppingCart.where(anonymous_user_id: @id)
ShoppingCart.where(anonymous_user_id: @id).order('created_at DESC')
end
def destroy_all_shopping_carts
@ -23,7 +24,11 @@ module JamRuby
end
def has_redeemable_jamtrack
true
APP_CONFIG.one_free_jamtrack_per_user && !@cookies[:redeemed_jamtrack]
end
def signup_hint
SignupHint.where(anonymous_user_id: @id).where('expires_at > ?', Time.now).first
end
end
end

View File

@ -44,7 +44,7 @@ module JamRuby
# this is basically a dev-time only path of code; we store real artifacts in s3
url = APP_CONFIG.jam_admin_root_url + self.uri.url
else
url = "http://#{APP_CONFIG.cloudfront_host}/#{self.uri.store_dir}/#{self[:uri]}"
url = "https://#{APP_CONFIG.cloudfront_host}/#{self.uri.store_dir}/#{self[:uri]}"
#url = self.uri.url.gsub(APP_CONFIG.aws_fullhost, APP_CONFIG.cloudfront_host)
end

View File

@ -163,14 +163,14 @@ module JamRuby
# ensure person creating this Band is a Musician
unless user.musician?
raise PermissionError, "must be a musician"
raise JamPermissionError, "must be a musician"
end
band = id.blank? ? Band.new : Band.find(id)
# ensure user updating Band details is a Band member
unless band.new_record? || band.users.exists?(user)
raise PermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR
raise JamPermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR
end
band.name = params[:name] if params.has_key?(:name)
@ -224,8 +224,8 @@ module JamRuby
:cropped_s3_path_photo => cropped_s3_path,
:cropped_large_s3_path_photo => cropped_large_s3_path,
:crop_selection_photo => crop_selection,
:photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => false),
:large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => false))
: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_photo(aws_bucket)

View File

@ -40,7 +40,7 @@ module JamRuby
# params is a hash, and everything is optional
def update_fields(user, params)
if user != self.user
raise PermissionError, "user doesn't own claimed_recording"
raise JamPermissionError, "user doesn't own claimed_recording"
end
self.name = params[:name]
@ -52,7 +52,7 @@ module JamRuby
def discard(user)
if user != self.user
raise PermissionError, "user doesn't own claimed_recording"
raise JamPermissionError, "user doesn't own claimed_recording"
end
ClaimedRecording.where(:id => id).update_all(:discarded => true )
@ -95,11 +95,11 @@ module JamRuby
target_user = params[:user]
raise PermissionError, "must specify current user" unless user
raise JamPermissionError, "must specify current user" unless user
raise "user must be specified" unless target_user
if target_user != user.id
raise PermissionError, "unable to view another user's favorites"
raise JamPermissionError, "unable to view another user's favorites"
end
query = ClaimedRecording.limit(limit).order('created_at DESC').offset(start)

View File

@ -24,11 +24,12 @@ module JamRuby
validates :metronome_open, :inclusion => {:in => [true, false]}
validates :as_musician, :inclusion => {:in => [true, false, nil]}
validates :client_type, :inclusion => {:in => CLIENT_TYPES}
validates_numericality_of :last_jam_audio_latency, greater_than:0, :allow_nil => true
validates_numericality_of :last_jam_audio_latency, greater_than: 0, :allow_nil => true
validate :can_join_music_session, :if => :joining_session?
validate :user_or_latency_tester_present
after_save :require_at_least_one_track_when_in_session, :if => :joining_session?
# this is no longer required with the new no-input profile
#after_save :require_at_least_one_track_when_in_session, :if => :joining_session?
after_create :did_create
after_save :report_add_participant
@ -62,11 +63,11 @@ module JamRuby
def state_message
case self.aasm_state.to_sym
when CONNECT_STATE
'Connected'
when STALE_STATE
'Stale'
'Connected'
when STALE_STATE
'Stale'
else
'Idle'
'Idle'
end
end
@ -85,7 +86,7 @@ module JamRuby
def joining_session?
joining_session
end
def can_join_music_session
# puts "can_join_music_session: #{music_session_id} was #{music_session_id_was}" if music_session_id_changed?
@ -183,8 +184,8 @@ module JamRuby
end
def associate_tracks(tracks)
self.tracks.clear()
unless tracks.nil?
self.tracks.clear()
tracks.each do |track|
t = Track.new
t.instrument = Instrument.find(track["instrument_id"])

View File

@ -0,0 +1,17 @@
module JamRuby
class FingerprintWhitelist < ActiveRecord::Base
@@log = Logging.logger[FingerprintWhitelist]
validates :fingerprint, presence: true, uniqueness: true
has_many :machine_fingerprint, class_name: 'JamRuby::MachineFingerprint', foreign_key: :fingerprint
def admin_url
APP_CONFIG.admin_root_url + "/admin/fingerprint_whitelists/" + id
end
def to_s
"#{fingerprint}"
end
end
end

View File

@ -0,0 +1,26 @@
module JamRuby
class FraudAlert < ActiveRecord::Base
@@log = Logging.logger[MachineExtra]
belongs_to :machine_fingerprint, :class_name => "JamRuby::MachineFingerprint"
belongs_to :user, :class_name => "JamRuby::User"
def self.create(machine_fingerprint, user)
fraud = FraudAlert.new
fraud.machine_fingerprint = machine_fingerprint
fraud.user = user
fraud.save
unless fraud.save
@@log.error("unable to create fraud: #{fraud.errors.inspect}")
end
fraud
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/fraud_alerts/" + id
end
end
end

View File

@ -23,9 +23,14 @@ module JamRuby
(database_environment == 'development' && Environment.mode == 'development')
end
def self.affiliate_tallied_at
GenericState.singleton.affiliate_tallied_at
end
def self.singleton
GenericState.find('default')
end
end
end

View File

@ -54,7 +54,7 @@ module JamRuby
mount.source_pass = APP_CONFIG.icecast_hardcoded_source_password || SecureRandom.urlsafe_base64
mount.stream_name = "JamKazam music session created by #{music_session.creator.name}"
mount.stream_description = music_session.description
mount.stream_url = "http://www.jamkazam.com" ## TODO/XXX, the jamkazam url should be the page hosting the widget
mount.stream_url = "https://www.jamkazam.com" ## TODO/XXX, the jamkazam url should be the page hosting the widget
mount.genre = music_session.genre.description
mount
end

View File

@ -17,7 +17,7 @@ module JamRuby
:original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price,
:reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount,
:licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes,
:jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, as: :admin
:jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, as: :admin
validates :name, presence: true, uniqueness: true, length: {maximum: 200}
validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 }
@ -60,7 +60,10 @@ module JamRuby
# has_many :plays, :class_name => "JamRuby::PlayablePlay", :foreign_key => :jam_track_id, :dependent => :destroy
# VRFS-2916 jam_tracks.id is varchar: ADD
has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy
# when we know what JamTrack this refund is related to, these are associated
belongs_to :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook'
accepts_nested_attributes_for :jam_track_tracks, allow_destroy: true
accepts_nested_attributes_for :jam_track_tap_ins, allow_destroy: true
@ -100,6 +103,7 @@ module JamRuby
warnings << 'POSITIONS' if duplicate_positions?
warnings << 'PREVIEWS'if missing_previews?
warnings << 'DURATION' if duration.nil?
warnings << 'JMEP' if jmep_json.blank?
warnings.join(',')
end
@ -112,6 +116,7 @@ module JamRuby
def all_artists
JamTrack.select("original_artist").
group("original_artist").
order('original_artist').
collect{|jam_track|jam_track.original_artist}
end
@ -161,16 +166,75 @@ module JamRuby
query = query.where("original_artist=?", options[:artist])
end
if options[:group_artist]
query = query.group("original_artist")
if options[:id].present?
query = query.where("jam_tracks.id=?", options[:id])
end
if options[:group_artist]
query = query.select("original_artist, array_agg(jam_tracks.id) AS id, MIN(name) AS name, MIN(description) AS description, MIN(recording_type) AS recording_type, MIN(original_artist) AS original_artist, MIN(songwriter) AS songwriter, MIN(publisher) AS publisher, MIN(sales_region) AS sales_region, MIN(price) AS price, MIN(version) AS version, MIN(genre_id) AS genre_id")
query = query.group("original_artist")
query = query.order('jam_tracks.original_artist')
else
query = query.group("jam_tracks.id")
query = query.order('jam_tracks.original_artist, jam_tracks.name')
end
query = query.where("jam_tracks.status = ?", 'Production') unless user.admin
query = query.where("jam_tracks.genre_id = '#{options[:genre]}'") unless options[:genre].blank?
query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type != 'Master'") unless options[:instrument].blank?
query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank?
if query.length == 0
[query, nil]
elsif query.length < limit
[query, nil]
else
[query, start + limit]
end
end
# provides artist names and how many jamtracks are available for each
def artist_index(options, user)
if options[:page]
page = options[:page].to_i
per_page = options[:per_page].to_i
if per_page == 0
# try and see if limit was specified
limit = options[:limit]
limit ||= 100
limit = limit.to_i
else
limit = per_page
end
start = (page -1 )* per_page
limit = per_page
else
limit = options[:limit]
limit ||= 100
limit = limit.to_i
start = options[:start].presence
start = start.to_i || 0
page = 1 + start/limit
per_page = limit
end
query = JamTrack.paginate(page: page, per_page: per_page)
query = query.select("original_artist, count(original_artist) AS song_count")
query = query.group("original_artist")
query = query.order('jam_tracks.original_artist')
query = query.where("jam_tracks.status = ?", 'Production') unless user.admin
query = query.where("jam_tracks.genre_id = '#{options[:genre]}'") unless options[:genre].blank?
query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}'") unless options[:instrument].blank?
query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank?
query = query.group("jam_tracks.id")
query = query.order('jam_tracks.name')
if query.length == 0
[query, nil]
@ -187,6 +251,10 @@ module JamRuby
JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Master').first
end
def stem_tracks
JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Track')
end
def can_download?(user)
owners.include?(user)
end
@ -194,5 +262,11 @@ module JamRuby
def right_for_user(user)
jam_track_rights.where("user_id=?", user).first
end
def short_plan_code
prefix = 'jamtrack-'
plan_code[prefix.length..-1]
end
end
end

View File

@ -3,21 +3,24 @@ module JamRuby
# describes what users have rights to which tracks
class JamTrackRight < ActiveRecord::Base
include JamRuby::S3ManagerMixin
attr_accessible :user, :jam_track, :user_id, :jam_track_id, :download_count
@@log = Logging.logger[JamTrackRight]
attr_accessible :user, :jam_track, :user_id, :jam_track_id, :download_count
attr_accessible :user_id, :jam_track_id, as: :admin
attr_accessible :url_48, :md5_48, :length_48, :url_44, :md5_44, :length_44
belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track
belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track
belongs_to :jam_track, class_name: "JamRuby::JamTrack"
validates :user, presence:true
validates :jam_track, presence:true
validates :is_test_purchase, inclusion: {in: [true, false]}
validates :user, presence: true
validates :jam_track, presence: true
validates :is_test_purchase, inclusion: {in: [true, false]}
validate :verify_download_count
after_save :after_save
validates_uniqueness_of :user_id, scope: :jam_track_id
validates_uniqueness_of :user_id, scope: :jam_track_id
# Uploads the JKZ:
mount_uploader :url_48, JamTrackRightUploader
mount_uploader :url_44, JamTrackRightUploader
@ -29,7 +32,7 @@ module JamRuby
# try to catch major transitions:
# if just queue time changes, start time changes, or signed time changes, send out a notice
if signing_queued_at_was != signing_queued_at || signing_started_at_was != signing_started_at || last_signed_at_was != last_signed_at
if signing_queued_at_was != signing_queued_at || signing_started_at_48_was != signing_started_at_48 || signing_started_at_44_was != signing_started_at_44 || last_signed_at_was != last_signed_at || current_packaging_step != current_packaging_step_was || packaging_steps != packaging_steps_was
SubscriptionMessage.jam_track_signing_job_change(self)
end
end
@ -39,10 +42,10 @@ module JamRuby
end
# create name of the file
def filename
"#{jam_track.name}.jkz"
def filename(bitrate)
"#{jam_track.name}-#{bitrate == :url_48 ? '48' : '44'}.jkz"
end
def verify_download_count
if (self.download_count < 0 || self.download_count > MAX_JAM_TRACK_DOWNLOADS) && !@current_user.admin
errors.add(:download_count, "must be less than or equal to #{MAX_JAM_TRACK_DOWNLOADS}")
@ -50,15 +53,21 @@ module JamRuby
end
def self.ready_to_clean
JamTrackRight.where("downloaded_since_sign=? AND updated_at <= ?", true, 5.minutes.ago).limit(1000)
JamTrackRight.where("downloaded_since_sign=? AND updated_at <= ?", true, 5.minutes.ago).limit(1000)
end
def finish_errored(error_reason, error_detail)
def finish_errored(error_reason, error_detail, sample_rate)
self.last_signed_at = Time.now
self.error_count = self.error_count + 1
self.error_reason = error_reason
self.error_detail = error_detail
self.should_retry = self.error_count < 5
if sample_rate == 48
self.signing_48 = false
else
self.signing_44 = false
end
if save
Notification.send_jam_track_sign_failed(self)
else
@ -71,20 +80,19 @@ module JamRuby
if bitrate==48
self.length_48 = length
self.md5_48 = md5
self.signed_48 = true
self.signing_48 = false
else
self.length_44 = length
self.md5_44 = md5
self.signed_44 = true
self.signing_44 = false
end
self.signed = true
self.error_count = 0
self.error_reason = nil
self.error_detail = nil
self.should_retry = false
if save
Notification.send_jam_track_sign_complete(self)
else
raise "Error sending notification #{self.errors}"
end
save!
end
# creates a short-lived URL that has access to the object.
@ -93,18 +101,18 @@ module JamRuby
# but the url is short lived enough so that it wouldn't be easily shared
def sign_url(expiration_time = 120, bitrate=48)
field_name = (bitrate==48) ? "url_48" : "url_44"
s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => false})
s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => true})
end
def delete_s3_files
remove_url_48!
remove_url_44!
remove_url_48!
remove_url_44!
end
def enqueue(sample_rate=48)
begin
JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at => nil, :last_signed_at => nil)
JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil)
Resque.enqueue(JamTracksBuilder, self.id, sample_rate)
true
rescue Exception => e
@ -116,7 +124,7 @@ module JamRuby
# if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off
def enqueue_if_needed(sample_rate=48)
state = signing_state
state = signing_state(sample_rate)
if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED'
false
else
@ -129,9 +137,9 @@ module JamRuby
# @return true if signed && file exists for the sample_rate specifed:
def ready?(sample_rate=48)
if sample_rate==48
self.signed && self.url_48.present? && self.url_48.file.exists?
self.signed_48 && self.url_48.present? && self.url_48.file.exists?
else
self.signed && self.url_44.present? && self.url_44.file.exists?
self.signed_44 && self.url_44.present? && self.url_44.file.exists?
end
end
@ -143,12 +151,28 @@ module JamRuby
# QUEUED_TIMEOUT - the package signing job (JamTrackBuilder) was queued, but never executed
# QUEUED - the package is queued to sign
# QUIET - the jam_track_right exists, but no job has been kicked off; a job needs to be enqueued
def signing_state
def signing_state(sample_rate = nil)
state = nil
# if the caller did not specified sample rate, we will determine what signing state to check by looking at the most recent signing attempt
if sample_rate.nil?
# determine what package is being signed by checking the most recent signing_started at
time_48 = signing_started_at_48.to_i
time_44 = signing_started_at_44.to_i
sample_rate = time_48 > time_44 ? 48 : 44
end
signed = sample_rate == 48 ? signed_48 : signed_44
signing_started_at = sample_rate == 48 ? signing_started_at_48 : signing_started_at_44
if signed
state = 'SIGNED'
elsif signing_started_at
if Time.now - signing_started_at > APP_CONFIG.signing_job_run_max_time
# the maximum amount of time the packaging job can take is 10 seconds * num steps. For a 10 track song, this will be 110 seconds. It's a bit long.
signing_job_run_max_time = packaging_steps * 10
if Time.now - signing_started_at > signing_job_run_max_time
state = 'SIGNING_TIMEOUT'
elsif Time.now - last_step_at > APP_CONFIG.signing_step_max_time
state = 'SIGNING_TIMEOUT'
else
state = 'SIGNING'
@ -166,9 +190,18 @@ module JamRuby
end
state
end
def signed?(sample_rate)
sample_rate == 48 ? signed_48 : signed_44
end
def update_download_count(count=1)
self.download_count = self.download_count + count
self.last_downloaded_at = Time.now
if self.signed_44 || self.signed_48
self.downloaded_since_sign = true
end
end
def self.list_keys(user, jamtracks)
@ -176,10 +209,144 @@ module JamRuby
return []
end
JamTrack.select('jam_tracks.id, jam_track_rights.private_key AS private_key, jam_track_rights.id AS jam_track_right_id')
JamTrack.select('jam_tracks.id, jam_track_rights.private_key_44 AS private_key_44, jam_track_rights.private_key_48 AS private_key_48, jam_track_rights.id AS jam_track_right_id')
.joins("LEFT OUTER JOIN jam_track_rights ON jam_tracks.id = jam_track_rights.jam_track_id AND jam_track_rights.user_id = '#{user.id}'")
.where('jam_tracks.id IN (?)', jamtracks)
end
def guard_against_fraud(current_user, fingerprint, remote_ip)
if current_user.blank?
return "no user specified"
end
# admin's get to skip fraud check
if current_user.admin
return nil
end
if fingerprint.nil? || fingerprint.empty?
return "no fingerprint specified"
end
all_fingerprint = fingerprint.delete(:all)
running_fingerprint = fingerprint.delete(:running)
if all_fingerprint.blank?
return "no all fingerprint specified"
end
if running_fingerprint.blank?
return "no running fingerprint specified"
end
all_fingerprint_extra = fingerprint[all_fingerprint]
running_fingerprint_extra = fingerprint[running_fingerprint]
if redeemed && !redeemed_and_fingerprinted
# if this is a free JamTrack, we need to check for fraud or accidental misuse
# first of all, does this user have any other JamTracks aside from this one that have already been redeemed it and are marked free?
other_redeemed_freebie = JamTrackRight.where(redeemed: true).where(redeemed_and_fingerprinted: true).where('id != ?', id).where(user_id: current_user.id).first
if other_redeemed_freebie
return "already redeemed another"
end
if FingerprintWhitelist.select('id').find_by_fingerprint(all_fingerprint)
# we can short circuit out of the rest of the check, since this is a known bad fingerprint
@@log.debug("ignoring 'all' hash found in whitelist")
else
# can we find a jam track that belongs to someone else with the same fingerprint
conflict = MachineFingerprint.select('count(id) as count').where('user_id != ?', current_user.id).where(fingerprint: all_fingerprint).where(remote_ip: remote_ip).where('created_at > ?', APP_CONFIG.expire_fingerprint_days.days.ago).first
conflict_count = conflict['count'].to_i
if conflict_count >= APP_CONFIG.found_conflict_count
mf = MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, all_fingerprint_extra, self)
# record the alert
fraud = FraudAlert.create(mf, current_user) if mf.valid?
fraud_admin_url = fraud.admin_url if fraud
AdminMailer.alerts(subject: "'All' fingerprint collision by #{current_user.name}",
body: "Current User: #{current_user.admin_url}\n\n Fraud Alert: #{fraud_admin_url}").deliver
# try to record the other fingerprint
mf = MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self)
if APP_CONFIG.error_on_fraud
return "other user has 'all' fingerprint"
else
self.redeemed_and_fingerprinted = true
save!
return nil
end
end
end
if all_fingerprint != running_fingerprint
if FingerprintWhitelist.select('id').find_by_fingerprint(running_fingerprint)
# we can short circuit out of the rest of the check, since this is a known bad fingerprint
@@log.debug("ignoring 'running' hash found in whitelist")
else
conflict = MachineFingerprint.select('count(id) as count').where('user_id != ?', current_user.id).where(fingerprint: running_fingerprint).where(remote_ip: remote_ip).where('created_at > ?', APP_CONFIG.expire_fingerprint_days.days.ago).first
conflict_count = conflict['count'].to_i
if conflict_count >= APP_CONFIG.found_conflict_count
mf = MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self)
# record the alert
fraud = FraudAlert.create(mf, current_user) if mf.valid?
fraud_admin_url = fraud.admin_url if fraud
AdminMailer.alerts(subject: "'Running' fingerprint collision by #{current_user.name}",
body: "Current User: #{current_user.admin_url}\n\nFraud Alert: #{fraud_admin_url}").deliver\
# try to record the other fingerprint
mf = MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, all_fingerprint_extra, self)
if APP_CONFIG.error_on_fraud
return "other user has 'running' fingerprint"
else
self.redeemed_and_fingerprinted = true
save!
return nil
end
end
end
end
# we made it past all checks; let's slap on the redeemed_fingerprint
self.redeemed_and_fingerprinted = true
MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, all_fingerprint_extra, self)
if all_fingerprint != running_fingerprint
MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self)
end
save!
end
nil
end
def self.stats
stats = {}
result = JamTrackRight.select('count(id) as total, count(CASE WHEN signing_44 THEN 1 ELSE NULL END) + count(CASE WHEN signing_48 THEN 1 ELSE NULL END) as signing_count, count(CASE WHEN redeemed THEN 1 ELSE NULL END) as redeem_count, count(last_downloaded_at) as redeemed_and_dl_count').where(is_test_purchase: false).first
stats['count'] = result['total'].to_i
stats['signing_count'] = result['signing_count'].to_i
stats['redeemed_count'] = result['redeem_count'].to_i
stats['redeemed_and_dl_count'] = result['redeemed_and_dl_count'].to_i
stats['purchased_count'] = stats['count'] - stats['redeemed_count']
stats
end
end
end

View File

@ -10,6 +10,7 @@ module JamRuby
@@log = Logging.logger[JamTrackTrack]
before_destroy :delete_s3_files
# Because JamTrackImporter imports audio files now, and because also the mere presence of this causes serious issues when updating the model (because reset of url_44 to something bogus), I've removed these
#mount_uploader :url_48, JamTrackTrackUploader
@ -18,7 +19,9 @@ module JamRuby
attr_accessible :jam_track_id, :track_type, :instrument, :instrument_id, :position, :part, as: :admin
attr_accessible :url_44, :url_48, :md5_44, :md5_48, :length_44, :length_48, :preview_start_time_raw, as: :admin
attr_accessor :original_audio_s3_path, :skip_uploader
attr_accessor :original_audio_s3_path, :skip_uploader, :preview_generate_error
before_destroy :delete_s3_files
validates :position, presence: true, numericality: {only_integer: true}, length: {in: 1..1000}
validates :part, length: {maximum: 25}
@ -57,7 +60,7 @@ module JamRuby
def preview_public_url(media_type='ogg')
url = media_type == 'ogg' ? self[:preview_url] : self[:preview_mp3_url]
if url
s3_public_manager.public_url(url,{ :secure => false})
s3_public_manager.public_url(url,{ :secure => true})
else
nil
end
@ -84,7 +87,7 @@ module JamRuby
# we would verify their rights (can_download?), and generates a URL in response to the click so that they can download
# but the url is short lived enough so that it wouldn't be easily shared
def sign_url(expiration_time = 120, sample_rate=48)
s3_manager.sign_url(url_by_sample_rate(sample_rate), {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
s3_manager.sign_url(url_by_sample_rate(sample_rate), {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => true})
end
def can_download?(user)
@ -120,7 +123,99 @@ module JamRuby
end
end
private
def delete_s3_files
s3_manager.delete(self[:url_44]) if self[:url_44] && s3_manager.exists?(self[:url_44])
s3_manager.delete(self[:url_48]) if self[:url_48] && s3_manager.exists?(self[:url_48])
s3_public_manager.delete(self[:preview_url]) if self[:preview_url] && s3_public_manager.exists?(self[:preview_url])
s3_public_manager.delete(self[:preview_mp3_url]) if self[:preview_mp3_url] && s3_public_manager.exists?(self[:preview_mp3_url])
end
def generate_preview
begin
Dir.mktmpdir do |tmp_dir|
input = File.join(tmp_dir, 'in.ogg')
output = File.join(tmp_dir, 'out.ogg')
output_mp3 = File.join(tmp_dir, 'out.mp3')
start = self.preview_start_time.to_f / 1000
stop = start + 20
raise 'no track' unless self["url_44"]
s3_manager.download(self.url_by_sample_rate(44), input)
command = "sox \"#{input}\" \"#{output}\" trim #{sprintf("%.3f", start)} =#{sprintf("%.3f", stop)}"
@@log.debug("trimming using: " + command)
sox_output = `#{command}`
result_code = $?.to_i
if result_code != 0
@@log.debug("fail #{result_code}")
@preview_generate_error = "unable to execute cut command #{sox_output}"
else
# now create mp3 off of ogg preview
convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\""
@@log.debug("converting to mp3 using: " + convert_mp3_cmd)
convert_output = `#{convert_mp3_cmd}`
result_code = $?.to_i
if result_code != 0
@@log.debug("fail #{result_code}")
@preview_generate_error = "unable to execute mp3 convert command #{convert_output}"
else
ogg_digest = ::Digest::MD5.file(output)
mp3_digest = ::Digest::MD5.file(output_mp3)
self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest
self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest
@@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}")
s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest)
@@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}")
s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest)
self.skip_uploader = true
original_ogg_preview_url = self["preview_url"]
original_mp3_preview_url = self["preview_mp3_url"]
# and finally update the JamTrackTrack with the new info
self["preview_url"] = self.preview_filename(ogg_md5, 'ogg')
self["preview_length"] = File.new(output).size
# and finally update the JamTrackTrack with the new info
self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3')
self["preview_mp3_length"] = File.new(output_mp3).size
self.save!
# if all that worked, now delete old previews, if present
begin
s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"]
s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"]
rescue
puts "UNABLE TO CLEANUP OLD PREVIEW URL"
end
end
end
end
rescue Exception => e
@@log.error("error in sox command #{e.to_s}")
@preview_generate_error = e.to_s
end
end
private
def normalize_position
parent = self.jam_track
position = 0

View File

@ -0,0 +1,35 @@
module JamRuby
class MachineExtra < ActiveRecord::Base
@@log = Logging.logger[MachineExtra]
belongs_to :machine_fingerprint, :class_name => "JamRuby::MachineFingerprint"
def self.create(machine_fingerprint, data)
me = MachineExtra.new
me.machine_fingerprint = machine_fingerprint
me.mac_address = data[:mac]
me.mac_name = data[:name]
me.upstate = data[:upstate]
me.ipaddr_0 = data[:ipaddr_0]
me.ipaddr_1 = data[:ipaddr_1]
me.ipaddr_2 = data[:ipaddr_2]
me.ipaddr_3 = data[:ipaddr_3]
me.ipaddr_4 = data[:ipaddr_4]
me.ipaddr_5 = data[:ipaddr_5]
me.save
unless me.save
@@log.error("unable to create machine extra: #{me.errors.inspect}")
end
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/machine_extras/" + id
end
def to_s
"#{mac_address} #{mac_name} #{upstate ? 'UP' : 'DOWN'} #{ipaddr_0} #{ipaddr_1} #{ipaddr_2} #{ipaddr_3} #{ipaddr_4} #{ipaddr_5}"
end
end
end

View File

@ -0,0 +1,49 @@
module JamRuby
class MachineFingerprint < ActiveRecord::Base
@@log = Logging.logger[MachineFingerprint]
belongs_to :user, :class_name => "JamRuby::User"
belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight"
has_one :detail, :class_name => "JamRuby::MachineExtra"
belongs_to :fingerprint_whitelist, class_name: 'JamRuby::FingerprintWhitelist', foreign_key: :fingerprint
TAKEN_ON_SUCCESSFUL_DOWNLOAD = 'dl'
TAKEN_ON_FRAUD_CONFLICT = 'fc'
PRINT_TYPE_ALL = 'a'
PRINT_TYPE_ACTIVE = 'r'
validates :user, presence:true
validates :when_taken, :inclusion => {:in => [TAKEN_ON_SUCCESSFUL_DOWNLOAD, TAKEN_ON_FRAUD_CONFLICT]}
validates :fingerprint, presence: true
validates :print_type, presence: true, :inclusion => {:in =>[PRINT_TYPE_ALL, PRINT_TYPE_ACTIVE]}
validates :remote_ip, presence: true
def self.create(fingerprint, user, when_taken, print_type, remote_ip, extra, jam_track_right = nil)
mf = MachineFingerprint.new
mf.fingerprint = fingerprint
mf.user = user
mf.when_taken = when_taken
mf.print_type = print_type
mf.remote_ip = remote_ip
mf.jam_track_right = jam_track_right
if mf.save
MachineExtra.create(mf, extra) if extra
else
@@log.error("unable to create machine fingerprint: #{mf.errors.inspect}")
end
mf
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/machine_fingerprints/" + id
end
def to_s
"#{fingerprint} #{remote_ip} #{user} #{detail}"
end
end
end

View File

@ -131,9 +131,10 @@ module JamRuby
end
end
uri = URI(sign_url(field))
url = sign_url(field)
uri = URI(url)
open downloaded_filename, 'wb' do |io|
Net::HTTP.start(uri.host, uri.port) do |http|
Net::HTTP.start(uri.host, uri.port, use_ssl: url.start_with?('https') ? true : false) do |http|
request = Net::HTTP::Get.new uri
http.request request do |response|
response_code = response.code.to_i

View File

@ -4,6 +4,8 @@ module JamRuby
MAX_MIX_TIME = 7200 # 2 hours
@@log = Logging.logger[Mix]
before_destroy :delete_s3_files
self.primary_key = 'id'
@ -137,15 +139,67 @@ module JamRuby
one_day = 60 * 60 * 24
jam_track_offset = 0
jam_track_seek = 0
was_jamtrack_played = false
if recording.timeline
recording_timeline_data = JSON.parse(recording.timeline)
# did the jam track play at all?
jam_track_isplaying = recording_timeline_data["jam_track_isplaying"]
recording_start_time = recording_timeline_data["recording_start_time"]
jam_track_play_start_time = recording_timeline_data["jam_track_play_start_time"]
jam_track_recording_start_play_offset = recording_timeline_data["jam_track_recording_start_play_offset"]
jam_track_offset = -jam_track_recording_start_play_offset
if jam_track_play_start_time != 0
was_jamtrack_played = true
# how long did the JamTrack play? not needed because we limit on the input tracks, which represents how long the recording is, too
jam_track_play_time = recording_timeline_data["jam_track_play_time"]
offset = jam_track_play_start_time - recording_start_time
@@log.debug("base offset = #{offset}")
if offset >= 0
# jamtrack started after recording, so buffer with silence as necessary\
if jam_track_recording_start_play_offset < 0
@@log.info("prelude captured. offsetting further by #{-jam_track_recording_start_play_offset}")
# a negative jam_track_recording_start_play_offset indicates prelude, i.e., silence
# so add it to the offset to add more silence as necessary
offset = offset + -jam_track_recording_start_play_offset
jam_track_offset = offset
else
@@log.info("positive jamtrack offset; seeking into jamtrack by #{jam_track_recording_start_play_offset}")
# a positive jam_track_recording_start_play_offset means we need to cut into the jamtrack
jam_track_seek = jam_track_recording_start_play_offset
jam_track_offset = offset
end
else
# jamtrack started before recording, so we can seek into it to make up for the missing parts
if jam_track_recording_start_play_offset < 0
@@log.info("partial prelude captured. offset becomes jamtrack offset#{-jam_track_recording_start_play_offset}")
# a negative jam_track_recording_start_play_offset indicates prelude, i.e., silence
# so add it to the offset to add more silence as necessary
jam_track_offset = -jam_track_recording_start_play_offset
else
@@log.info("no prelude captured. offset becomes jamtrack offset=#{jam_track_recording_start_play_offset}")
jam_track_offset = 0
jam_track_seek = jam_track_recording_start_play_offset
end
# also, ignore jam_track_recording_start_play_offset - it simply matches the offset in this case
end
@@log.info("computed values. jam_track_offset=#{jam_track_offset} jam_track_seek=#{jam_track_seek}")
end
end
manifest = { "files" => [], "timeline" => [] }
@ -154,7 +208,7 @@ module JamRuby
# this 'pick limiter' logic will ensure that we set a limiter on the 1st recorded_track we come across.
pick_limiter = false
if recording.is_jamtrack_recording?
if was_jamtrack_played
# we only use the limiter feature if this is a JamTrack recording
# by setting this to true, the 1st recorded_track in the database will be the limiter
pick_limiter = true
@ -171,27 +225,29 @@ module JamRuby
mix_params << { "level" => 1.0, "balance" => 0 }
end
recording.recorded_jam_track_tracks.each do |recorded_jam_track_track|
manifest["files"] << { "filename" => recorded_jam_track_track.jam_track_track.sign_url(one_day), "codec" => "vorbis", "offset" => jam_track_offset }
# let's look for level info from the client
level = 1.0 # default value - means no effect
if recorded_jam_track_track.timeline
if was_jamtrack_played
recording.recorded_jam_track_tracks.each do |recorded_jam_track_track|
manifest["files"] << { "filename" => recorded_jam_track_track.jam_track_track.sign_url(one_day, sample_rate=44), "codec" => "vorbis", "offset" => jam_track_offset, "seek" => jam_track_seek }
# let's look for level info from the client
level = 1.0 # default value - means no effect
if recorded_jam_track_track.timeline
timeline_data = JSON.parse(recorded_jam_track_track.timeline)
timeline_data = JSON.parse(recorded_jam_track_track.timeline)
# always take the 1st entry for now
first = timeline_data[0]
# always take the 1st entry for now
first = timeline_data[0]
if first["mute"]
# mute equates to no noise
level = 0.0
else
# otherwise grab the left channel...
level = first["vol_l"]
if first["mute"]
# mute equates to no noise
level = 0.0
else
# otherwise grab the left channel...
level = first["vol_l"]
end
end
end
mix_params << { "level" => level, "balance" => 0 }
mix_params << { "level" => level, "balance" => 0 }
end
end
manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }
@ -200,14 +256,47 @@ module JamRuby
manifest
end
def local_manifest
remote_manifest = self.manifest
remote_manifest["files"].each do |file|
filename = file["filename"]
basename = File.basename(filename)
basename = basename[0..(basename.index('?') - 1)]
file["filename"] = basename
end
# update manifest so that audiomixer writes here
remote_manifest["output"]["filename"] = 'out.ogg'
# update manifest so that audiomixer writes here
remote_manifest["error_out"] = 'error.out'
remote_manifest["mix_id"] = self.id
remote_manifest
end
def download_script
out = ''
remote_manifest = manifest
remote_manifest["files"].each do |file|
filename = file["filename"]
basename = File.basename(filename)
basename = basename[0..(basename.index('?') - 1)]
out << "curl -o \"#{basename}\" \"#{filename}\"\r\n\r\n"
end
out << "\r\n\r\n"
out
end
def s3_url(type='ogg')
if type == 'ogg'
s3_manager.s3_url(self[:ogg_url])
else
s3_manager.s3_url(self[:mp3_url])
end
end
def is_completed
@ -216,7 +305,7 @@ module JamRuby
# if the url starts with http, just return it because it's in some other store. Otherwise it's a relative path in s3 and needs be signed
def resolve_url(url_field, mime_type, expiration_time)
self[url_field].start_with?('http') ? self[url_field] : s3_manager.sign_url(self[url_field], {:expires => expiration_time, :response_content_type => mime_type, :secure => false})
self[url_field].start_with?('http') ? self[url_field] : s3_manager.sign_url(self[url_field], {:expires => expiration_time, :response_content_type => mime_type, :secure => true})
end
def sign_url(expiration_time = 120, type='ogg')

View File

@ -39,7 +39,7 @@ module JamRuby
end
def sign_url(expiration_time = 120)
s3_manager.sign_url(self[:file_url], {:expires => expiration_time, :secure => false})
s3_manager.sign_url(self[:file_url], {:expires => expiration_time, :secure => true})
end
private

View File

@ -282,7 +282,7 @@ module JamRuby
return query
end
def self.scheduled user
def self.scheduled user, only_public = false
# keep unstarted sessions around for 12 hours after scheduled_start
session_not_started = "(music_sessions.scheduled_start > NOW() - '12 hour'::INTERVAL AND music_sessions.started_at IS NULL)"
@ -293,6 +293,7 @@ module JamRuby
session_finished = "(music_sessions.session_removed_at > NOW() - '2 hour'::INTERVAL)"
query = MusicSession.where("music_sessions.canceled = FALSE")
query = query.where('music_sessions.fan_access = TRUE or music_sessions.musician_access = TRUE') if only_public
query = query.where("music_sessions.user_id = '#{user.id}'")
query = query.where("music_sessions.scheduled_start IS NULL OR #{session_not_started} OR #{session_finished} OR #{session_started_not_finished}")
query = query.where("music_sessions.create_type IS NULL OR music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}'")

View File

@ -43,7 +43,7 @@ module JamRuby
session_user_history.music_session_id = music_session_id
session_user_history.user_id = user_id
session_user_history.client_id = client_id
session_user_history.instruments = tracks.map {|t| t[:instrument_id]}.join("|")
session_user_history.instruments = tracks.map {|t| t[:instrument_id]}.join("|") if tracks
session_user_history.save
end

View File

@ -210,7 +210,7 @@ module JamRuby
return "New message about session."
when NotificationTypes::JAM_TRACK_SIGN_COMPLETE
return "Jam Track is ready for download."
return "JamTrack is ready for download."
# recording notifications
when NotificationTypes::MUSICIAN_RECORDING_SAVED

View File

@ -0,0 +1,39 @@
module JamRuby
class PaymentHistory < ActiveRecord::Base
self.table_name = 'payment_histories'
belongs_to :sale
belongs_to :recurly_transaction_web_hook
def self.index(user, params = {})
limit = params[:per_page]
limit ||= 20
limit = limit.to_i
query = PaymentHistory.limit(limit)
.includes(sale: [:sale_line_items], recurly_transaction_web_hook:[])
.where(user_id: user.id)
.where("transaction_type = 'sale' OR transaction_type = 'refund' OR transaction_type = 'void'")
.order('created_at DESC')
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
end
end

View File

@ -219,7 +219,7 @@ module JamRuby
# if the url starts with http, just return it because it's in some other store. Otherwise it's a relative path in s3 and needs be signed
def resolve_url(url_field, mime_type, expiration_time)
self[url_field].start_with?('http') ? self[url_field] : s3_manager.sign_url(self[url_field], {:expires => expiration_time, :response_content_type => mime_type, :secure => false})
self[url_field].start_with?('http') ? self[url_field] : s3_manager.sign_url(self[url_field], {:expires => expiration_time, :response_content_type => mime_type, :secure => true})
end
def sign_url(expiration_time = 120, type='ogg')
@ -232,6 +232,7 @@ module JamRuby
end
end
# this is not 'secure' because, in testing, the PUT failed often in Ruby. should investigate more.
def sign_put(expiration_time = 3600 * 24, type='ogg')
type ||= 'ogg'
if type == 'ogg'

View File

@ -41,7 +41,7 @@ module JamRuby
end
def sign_url(expiration_time = 120)
s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => true})
end
def can_download?(some_user)

View File

@ -148,7 +148,7 @@ module JamRuby
end
def sign_url(expiration_time = 120)
s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => true})
end
def upload_start(length, md5)

View File

@ -3,7 +3,7 @@ module JamRuby
@@log = Logging.logger[Recording]
attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, as: :admin
attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, :jam_track_id, as: :admin
has_many :users, :through => :recorded_tracks, :class_name => "JamRuby::User"
has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy
@ -21,6 +21,7 @@ module JamRuby
belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings, :foreign_key => 'owner_id'
belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings
belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :recordings, foreign_key: :music_session_id
belongs_to :non_active_music_session, :class_name => "JamRuby::MusicSession", foreign_key: :music_session_id
belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :inverse_of => :recordings, :foreign_key => 'jam_track_id'
belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :initiated_jam_track_recordings, :foreign_key => 'jam_track_initiator_id'
@ -50,7 +51,11 @@ module JamRuby
end
def is_jamtrack_recording?
!jam_track_id.nil?
!jam_track_id.nil? && parsed_timeline['jam_track_isplaying']
end
def parsed_timeline
timeline ? JSON.parse(timeline) : {}
end
def high_quality_mix?
@ -182,21 +187,21 @@ module JamRuby
def recorded_tracks_for_user(user)
unless self.users.exists?(user)
raise PermissionError, "user was not in this session"
raise JamPermissionError, "user was not in this session"
end
recorded_tracks.where(:user_id => user.id)
end
def recorded_backing_tracks_for_user(user)
unless self.users.exists?(user)
raise PermissionError, "user was not in this session"
raise JamPermissionError, "user was not in this session"
end
recorded_backing_tracks.where(:user_id => user.id)
end
def has_access?(user)
users.exists?(user)
users.exists?(user) || plays.where("player_id=?", user).count != 0
end
# Start recording a session.
@ -264,7 +269,7 @@ module JamRuby
def claim(user, name, description, genre, is_public, upload_to_youtube=false)
upload_to_youtube = !!upload_to_youtube # Correct where nil is borking save
unless self.users.exists?(user)
raise PermissionError, "user was not in this session"
raise JamPermissionError, "user was not in this session"
end
claimed_recording = ClaimedRecording.new
@ -710,23 +715,26 @@ module JamRuby
end
end
def self.popular_recordings(limit = 100)
Recording.select('recordings.id').joins('inner join claimed_recordings ON claimed_recordings.recording_id = recordings.id AND claimed_recordings.is_public = TRUE').where(all_discarded: false).where(is_done: true).where(deleted: false).order('play_count DESC').limit(limit).group('recordings.id')
end
private
def self.validate_user_is_band_member(user, band)
unless band.users.exists? user
raise PermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR
raise JamPermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR
end
end
def self.validate_user_is_creator(user, creator)
unless user.id == creator.id
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
end
def self.validate_user_is_musician(user)
unless user.musician?
raise PermissionError, ValidationMessages::USER_NOT_MUSICIAN_VALIDATION_ERROR
raise JamPermissionError, ValidationMessages::USER_NOT_MUSICIAN_VALIDATION_ERROR
end
end

View File

@ -1,20 +1,42 @@
module JamRuby
class RecurlyTransactionWebHook < ActiveRecord::Base
class RecurlyTransactionWebHook < ActiveRecord::Base
attr_accessible :admin_description, :jam_track_id, as: :admin
belongs_to :user, class_name: 'JamRuby::User'
belongs_to :sale_line_item, class_name: 'JamRuby::SaleLineItem', foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid', inverse_of: :recurly_transactions
belongs_to :sale, class_name: 'JamRuby::Sale', foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id', inverse_of: :recurly_transactions
# when we know what JamTrack this refund is related to, we set this value
belongs_to :jam_track, class_name: 'JamRuby::JamTrack'
validates :recurly_transaction_id, presence: true
validates :subscription_id, presence: true
validates :action, presence: true
validates :status, presence: true
validates :amount_in_cents, numericality: {only_integer: true}
validates :user, presence: true
SUCCESSFUL_PAYMENT = 'payment'
FAILED_PAYMENT = 'failed_payment'
REFUND = 'refund'
VOID = 'void'
HOOK_TYPES = [SUCCESSFUL_PAYMENT, FAILED_PAYMENT, REFUND, VOID]
def is_credit_type?
transaction_type == REFUND || transaction_type == VOID
end
def is_voided?
transaction_type == VOID
end
def is_refund?
transaction_type == REFUND
end
def self.is_transaction_web_hook?(document)
return false if document.root.nil?
@ -32,6 +54,10 @@ module JamRuby
end
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/recurly_hooks/" + id
end
# see spec for examples of XML
def self.create_from_xml(document)
@ -67,9 +93,60 @@ module JamRuby
# now that we have the transaction saved, we also need to delete the jam_track_right if this is a refund, or voided
if transaction.transaction_type == 'refund' || transaction.transaction_type == 'void'
right = JamTrackRight.find_by_recurly_subscription_uuid(transaction.subscription_id)
right.destroy if right
sale = Sale.find_by_recurly_invoice_id(transaction.invoice_id)
if sale && sale.is_jam_track_sale?
if sale.sale_line_items.length == 1
if sale.recurly_total_in_cents == transaction.amount_in_cents
line_item = sale.sale_line_items[0]
jam_track = line_item.product
jam_track_right = jam_track.right_for_user(transaction.user) if jam_track
if jam_track_right
line_item.affiliate_refunded = true
line_item.affiliate_refunded_at = Time.now
line_item.save!
jam_track_right.destroy
# associate which JamTrack we assume this is related to in this one success case
transaction.jam_track = jam_track
transaction.save!
AdminMailer.recurly_alerts(transaction.user, {
subject: "NOTICE: #{transaction.user.email} has had JamTrack: #{jam_track.name} revoked",
body: "A #{transaction.transaction_type} event came from Recurly for sale with Recurly invoice ID #{sale.recurly_invoice_id}. We deleted their right to the track in our own database as a result."
}).deliver
else
AdminMailer.recurly_alerts(transaction.user, {
subject: "NOTICE: #{transaction.user.email} got a refund, but unable to find JamTrackRight to delete",
body: "This should just mean the user already has no rights to the JamTrackRight when the refund came in. Not a big deal, but sort of weird..."
}).deliver
end
else
AdminMailer.recurly_alerts(transaction.user, {
subject: "ACTION REQUIRED: #{transaction.user.email} got a refund it was not for total value of a JamTrack sale",
body: "We received a #{transaction.transaction_type} notice for an amount that was not the same as the original sale. So, no action was taken in the database. sale total: #{sale.recurly_total_in_cents}, refund amount: #{transaction.amount_in_cents}"
}).deliver
end
else
AdminMailer.recurly_alerts(transaction.user, {
subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice with multiple JamTracks",
body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks"
}).deliver
end
else
AdminMailer.recurly_alerts(transaction.user, {
subject: "ACTION REQUIRED: #{transaction.user.email} has refund with no correlator to sales",
body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks"
}).deliver
end
end
transaction
end

View File

@ -62,7 +62,7 @@ module JamRuby
invitation = Invitation.where("music_session_id = ? AND receiver_id = ?", music_session.id, user.id)
if invitation.first.nil? && !music_session.open_rsvps && music_session.creator.id != user.id
raise PermissionError, "Only a session invitee can create an RSVP for this session."
raise JamPermissionError, "Only a session invitee can create an RSVP for this session."
end
RsvpRequest.transaction do
@ -154,7 +154,7 @@ module JamRuby
# authorize the user attempting to respond to the RSVP request
if music_session.creator.id != user.id
raise PermissionError, "Only the session organizer can accept or decline an RSVP request."
raise JamPermissionError, "Only the session organizer can accept or decline an RSVP request."
end
rsvp_request = RsvpRequest.find_by_id(rsvp_request_id)
@ -249,7 +249,7 @@ module JamRuby
rsvp_request = RsvpRequest.find(params[:id])
if music_session.creator.id != user.id && rsvp_request.user_id != user.id
raise PermissionError, "Only the session organizer or RSVP creator can cancel the RSVP."
raise JamPermissionError, "Only the session organizer or RSVP creator can cancel the RSVP."
end
RsvpRequest.transaction do

View File

@ -3,30 +3,370 @@ module JamRuby
# a sale is created every time someone tries to buy something
class Sale < ActiveRecord::Base
JAMTRACK_SALE = 'jamtrack'
belongs_to :user, class_name: 'JamRuby::User'
has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem'
validates :order_total, numericality: { only_integer: false }
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
def self.create(user)
@@log = Logging.logger[Sale]
def self.index(user, params = {})
limit = params[:per_page]
limit ||= 20
limit = limit.to_i
query = Sale.limit(limit)
.includes([:recurly_transactions, :sale_line_items])
.where('sales.user_id' => user.id)
.order('sales.created_at DESC')
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 state
original_total = self.recurly_total_in_cents
is_voided = false
refund_total = 0
recurly_transactions.each do |transaction|
if transaction.is_voided?
is_voided = true
else
end
if transaction.is_refund?
refund_total = refund_total + transaction.amount_in_cents
end
end
# if refund_total is > 0, then you have a refund.
# if voided is true, then in theory the whole thing has been refunded
{
voided: is_voided,
original_total: original_total,
refund_total: refund_total
}
end
def self.preview_invoice(current_user, shopping_carts)
line_items = {jam_tracks: []}
shopping_carts_jam_tracks = []
shopping_carts_subscriptions = []
shopping_carts.each do |shopping_cart|
if shopping_cart.is_jam_track?
shopping_carts_jam_tracks << shopping_cart
else
# XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
shopping_carts_subscriptions << shopping_cart
end
end
jam_track_items = preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
line_items[:jam_tracks] = jam_track_items if jam_track_items
# TODO: process shopping_carts_subscriptions
line_items
end
# place_order will create one or more sales based on the contents of shopping_carts for the current user
# individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation)
# jamtracks however can be piled onto the same sale as adjustments (VRFS-3028)
# so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack
def self.place_order(current_user, shopping_carts)
sales = []
shopping_carts_jam_tracks = []
shopping_carts_subscriptions = []
shopping_carts.each do |shopping_cart|
if shopping_cart.is_jam_track?
shopping_carts_jam_tracks << shopping_cart
else
# XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
shopping_carts_subscriptions << shopping_cart
end
end
jam_track_sale = order_jam_tracks(current_user, shopping_carts_jam_tracks)
sales << jam_track_sale if jam_track_sale
# TODO: process shopping_carts_subscriptions
sales
end
def self.preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
### XXX TODO;
# we currently use a fake plan in Recurly to estimate taxes using the Pricing.Attach metod in Recurly.js
# if we were to implement this the right way (ensure adjustments are on the account as necessary), then it would be better (more correct)
# just a pain to implement
end
def self.is_only_freebie(shopping_carts_jam_tracks)
shopping_carts_jam_tracks.length == 1 && shopping_carts_jam_tracks[0].product_info[:free]
end
# this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed)
# it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned
def self.order_jam_tracks(current_user, shopping_carts_jam_tracks)
client = RecurlyClient.new
sale = nil
Sale.transaction do
sale = create_jam_track_sale(current_user)
if sale.valid?
if is_only_freebie(shopping_carts_jam_tracks)
sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, nil)
sale.recurly_subtotal_in_cents = 0
sale.recurly_tax_in_cents = 0
sale.recurly_total_in_cents = 0
sale.recurly_currency = 'USD'
sale_line_item = sale.sale_line_items[0]
sale_line_item.recurly_tax_in_cents = 0
sale_line_item.recurly_total_in_cents = 0
sale_line_item.recurly_currency = 'USD'
sale_line_item.recurly_discount_in_cents = 0
sale.save
else
account = client.get_account(current_user)
if account.present?
purge_pending_adjustments(account)
created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
# now invoice the sale ... almost done
begin
invoice = account.invoice!
sale.recurly_invoice_id = invoice.uuid
sale.recurly_invoice_number = invoice.invoice_number
# now slap in all the real tax/purchase totals
sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents
sale.recurly_tax_in_cents = invoice.tax_in_cents
sale.recurly_total_in_cents = invoice.total_in_cents
sale.recurly_currency = invoice.currency
# and resolve against sale_line_items
sale.sale_line_items.each do |sale_line_item|
found_line_item = false
invoice.line_items.each do |line_item|
if line_item.uuid == sale_line_item.recurly_adjustment_uuid
sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents
sale_line_item.recurly_total_in_cents =line_item.total_in_cents
sale_line_item.recurly_currency = line_item.currency
sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents
found_line_item = true
break
end
end
if !found_line_item
@@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}")
puts "CANT FIND LINE ITEM"
end
end
unless sale.save
raise RecurlyClientError, "Invalid sale (at end)."
end
rescue Recurly::Resource::Invalid => e
# this exception is thrown by invoice! if the invoice is invalid
sale.rollback_adjustments(current_user, created_adjustments)
sale = nil
raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic
end
else
raise RecurlyClientError, "Could not find account to place order."
end
end
else
raise RecurlyClientError, "Invalid sale."
end
end
sale
end
def process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
created_adjustments = []
begin
shopping_carts_jam_tracks.each do |shopping_cart|
process_jam_track(current_user, shopping_cart, account, created_adjustments)
end
rescue Recurly::Error, NoMethodError => x
# rollback any adjustments created if error
rollback_adjustments(user, created_adjustments)
raise RecurlyClientError, x.to_s
rescue Exception => e
# rollback any adjustments created if error
rollback_adjustments(user, created_adjustments)
raise e
end
created_adjustments
end
def process_jam_track(current_user, shopping_cart, account, created_adjustments)
recurly_adjustment_uuid = nil
recurly_adjustment_credit_uuid = nil
# we do this because of ShoppingCart.remove_jam_track_from_cart; if it occurs, which should be rare, we need fresh shopping cart info
shopping_cart.reload
# get the JamTrack in this shopping cart
jam_track = shopping_cart.cart_product
if jam_track.right_for_user(current_user)
# if the user already owns the JamTrack, we should just skip this cart item, and destroy it
# if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop
ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart)
return
end
if account
# ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
adjustments = shopping_cart.create_adjustment_attributes(current_user)
adjustments.each do |adjustment|
# create the adjustment at Recurly (this may not look like it, but it is a REST API)
created_adjustment = account.adjustments.new(adjustment)
created_adjustment.save
# if the adjustment could not be made, bail
raise RecurlyClientError.new(created_adjustment.errors) if created_adjustment.errors.any?
# keep track of adjustments we created for this order, in case we have to roll them back
created_adjustments << created_adjustment
if ShoppingCart.is_product_purchase?(adjustment)
# this was a normal product adjustment, so track it as such
recurly_adjustment_uuid = created_adjustment.uuid
else
# this was a 'credit' adjustment, so track it as such
recurly_adjustment_credit_uuid = created_adjustment.uuid
end
end
end
# create one sale line item for every jam track
sale_line_item = SaleLineItem.create_from_shopping_cart(self, shopping_cart, nil, recurly_adjustment_uuid, recurly_adjustment_credit_uuid)
# if the sale line item is invalid, blow up the transaction
unless sale_line_item.valid?
@log.error("sale item invalid! #{sale_line_item.errors.inspect}")
puts("sale item invalid! #{sale_line_item.errors.inspect}")
Stats.write('web.recurly.purchase.sale_invalid', {message: sale_line_item.errors.to_s, value: 1})
raise RecurlyClientError.new(sale_line_item.errors)
end
# create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident)
jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
jam_track_right.redeemed = shopping_cart.free?
end
# also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks
if shopping_cart.free?
User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false)
current_user.has_redeemable_jamtrack = false # make sure model reflects the truth
end
# this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid
jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid
jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
unless jam_track_right.save
raise RecurlyClientError.new(jam_track_right.errors)
end
end
# delete the shopping cart; it's been dealt with
shopping_cart.destroy if shopping_cart
# blow up the transaction if the JamTrackRight did not get created
raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?
end
def rollback_adjustments(current_user, adjustments)
begin
adjustments.each { |adjustment| adjustment.destroy }
rescue Exception => e
AdminMailer.alerts({
subject: "ACTION REQUIRED: #{current_user.email} did not have all of his adjustments destroyed in rollback",
body: "go delete any adjustments on the account that don't belong. error: #{e}\n\nAdjustments: #{adjustments.inspect}"
}).deliver
end
end
def self.purge_pending_adjustments(account)
account.adjustments.pending.find_each do |adjustment|
# we only pre-emptively destroy pending adjustments if they appear to be created by the server
adjustment.destroy if ShoppingCart.is_server_pending_adjustment?(adjustment)
end
end
def is_jam_track_sale?
sale_type == JAMTRACK_SALE
end
def self.create_jam_track_sale(user)
sale = Sale.new
sale.user = user
sale.sale_type = JAMTRACK_SALE
sale.order_total = 0
sale.save
sale
end
def self.check_integrity
SaleLineItem.select([:total, :not_known, :succeeded, :failed, :refunded, :voided]).find_by_sql(
"SELECT COUNT(sale_line_items.id) AS total,
COUNT(CASE WHEN transactions.id IS NULL THEN 1 ELSE null END) not_known,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT}' THEN 1 ELSE null END) succeeded,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::FAILED_PAYMENT}' THEN 1 ELSE null END) failed,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::REFUND}' THEN 1 ELSE null END) refunded,
# this checks just jamtrack sales appropriately
def self.check_integrity_of_jam_track_sales
Sale.select([:total, :voided]).find_by_sql(
"SELECT COUNT(sales.id) AS total,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::VOID}' THEN 1 ELSE null END) voided
FROM sale_line_items
LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON subscription_id = recurly_subscription_uuid")
FROM sales
LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON invoice_id = sales.recurly_invoice_id
WHERE sale_type = '#{JAMTRACK_SALE}'")
end
end
end

View File

@ -1,27 +1,70 @@
module JamRuby
class SaleLineItem < ActiveRecord::Base
belongs_to :sale, class_name: 'JamRuby::Sale'
belongs_to :jam_track, class_name: 'JamRuby::JamTrack'
belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight'
JAMBLASTER = 'JamBlaster'
JAMCLOUD = 'JamCloud'
JAMTRACK = 'JamTrack'
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 :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id
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]}
validates :unit_price, numericality: {only_integer: false}
validates :quantity, numericality: {only_integer: true}
validates :free, numericality: {only_integer: true}
validates :sales_tax, numericality: {only_integer: false}, allow_nil: true
validates :shipping_handling, numericality: {only_integer: false}
validates :affiliate_referral_fee_in_cents, numericality: {only_integer: false}, allow_nil: true
validates :recurly_plan_code, presence:true
validates :sale, presence:true
def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid)
def product
if product_type == JAMTRACK
JamTrack.find_by_id(product_id)
else
raise 'unsupported product type'
end
end
def product_info
item = product
{ name: product.name } if item
end
def state
voided = false
refunded = false
failed = false
succeeded = false
recurly_transactions.each do |transaction|
if transaction.transaction_type == RecurlyTransactionWebHook::VOID
voided = true
elsif transaction.transaction_type == RecurlyTransactionWebHook::REFUND
refunded = true
elsif transaction.transaction_type == RecurlyTransactionWebHook::FAILED_PAYMENT
failed = true
elsif transaction.transaction_type == RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT
succeeded = true
end
end
{
void: voided,
refund: refunded,
fail: failed,
success: succeeded
}
end
def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid)
product_info = shopping_cart.product_info
sale.order_total = sale.order_total + product_info[:total_price]
sale.order_total = sale.order_total + product_info[:real_price]
sale_line_item = SaleLineItem.new
sale_line_item.product_type = shopping_cart.cart_type
@ -33,7 +76,19 @@ module JamRuby
sale_line_item.recurly_plan_code = product_info[:plan_code]
sale_line_item.product_id = shopping_cart.cart_id
sale_line_item.recurly_subscription_uuid = recurly_subscription_uuid
sale_line_item.sale = sale
sale_line_item.recurly_adjustment_uuid = recurly_adjustment_uuid
sale_line_item.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
# determine if we need to associate this sale with a partner
user = shopping_cart.user
referral_info = user.should_attribute_sale?(shopping_cart)
if referral_info
sale_line_item.affiliate_referral = user.affiliate_referral
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

View File

@ -1,8 +1,19 @@
module JamRuby
class ShoppingCart < ActiveRecord::Base
# just a normal purchase; used on the description field of a recurly adjustment
PURCHASE_NORMAL = 'purchase-normal'
# a free purchase; used on the description field of a recurly adjustment
PURCHASE_FREE = 'purchase-free'
# a techinicality of Recurly; we create a free-credit adjustment to balance out the free purchase adjustment
PURCHASE_FREE_CREDIT = 'purchase-free-credit'
PURCHASE_REASONS = [PURCHASE_NORMAL, PURCHASE_FREE, PURCHASE_FREE_CREDIT]
attr_accessible :quantity, :cart_type, :product_info
validates_uniqueness_of :cart_id, scope: [:cart_type, :user_id, :anonymous_user_id]
belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id"
validates :cart_id, presence: true
@ -14,14 +25,20 @@ module JamRuby
def product_info
product = self.cart_product
{name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem} unless product.nil?
{name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region} unless product.nil?
end
# multiply quantity by price
def total_price(product)
quantity * product.price
end
# multiply (quantity - redeemable) by price
def total_price(product)
def real_price(product)
(quantity - marked_for_redeem) * product.price
end
def cart_product
self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank?
end
@ -51,6 +68,68 @@ module JamRuby
cart
end
def is_jam_track?
cart_type == JamTrack::PRODUCT_TYPE
end
# returns an array of adjustments for the shopping cart
def create_adjustment_attributes(current_user)
raise "not a jam track" unless is_jam_track?
info = self.product_info
if free?
# create the credit, then the pseudo charge
[
{
accounting_code: PURCHASE_FREE_CREDIT,
currency: 'USD',
unit_amount_in_cents: -(info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name] + " (Credit)",
tax_exempt: true
},
{
accounting_code: PURCHASE_FREE,
currency: 'USD',
unit_amount_in_cents: (info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name],
tax_exempt: true
}
]
else
[
{
accounting_code: PURCHASE_NORMAL,
currency: 'USD',
unit_amount_in_cents: (info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name],
tax_exempt: false
}
]
end
end
def self.move_to_user(user, anonymous_user, shopping_carts)
shopping_carts.each do |shopping_cart|
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user)
cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem)
end
anonymous_user.destroy_all_shopping_carts
end
def self.is_product_purchase?(adjustment)
(adjustment[:accounting_code].include?(PURCHASE_FREE) || adjustment[:accounting_code].include?(PURCHASE_NORMAL)) && !adjustment[:accounting_code].include?(PURCHASE_FREE_CREDIT)
end
# recurly_adjustment is a Recurly::Adjustment (http://www.rubydoc.info/gems/recurly/Recurly/Adjustment)
# this asks, 'is this a pending adjustment?' AND 'was this adjustment created by the server (vs manually by someone -- we should leave those alone).'
def self.is_server_pending_adjustment?(recurly_adjustment)
recurly_adjustment.state == 'pending' && (recurly_adjustment.accounting_code.include?(PURCHASE_FREE) || recurly_adjustment.accounting_code.include?(PURCHASE_NORMAL) || recurly_adjustment.accounting_code.include?(PURCHASE_FREE_CREDIT))
end
# if the user has a redeemable jam_track still on their account, then also check if any shopping carts have already been marked.
# if no shpping carts have been marked, then mark it redeemable
# should be wrapped in a TRANSACTION
@ -73,20 +152,14 @@ module JamRuby
def self.add_jam_track_to_cart(any_user, jam_track)
cart = nil
ShoppingCart.transaction do
# does this user already have this JamTrack in their cart? If so, don't add it.
duplicate_found = false
any_user.shopping_carts.each do |shopping_cart|
if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE && shopping_cart.cart_id == jam_track.id
duplicate_found = true
return
end
if any_user.has_redeemable_jamtrack
# if you still have a freebie available to you, or if you are an anonymous user, we make sure there is nothing else in your shopping cart
any_user.destroy_all_shopping_carts
end
unless duplicate_found
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem)
end
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem)
end
cart
end
@ -106,7 +179,13 @@ module JamRuby
carts[0].save
end
end
end
def port(user, anonymous_user)
ShoppingCart.transaction do
move_to_user(user, anonymous_user, anonymous_user.shopping_carts)
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More