app_earnings 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 487fedffadd25385824a6975622541ccfc1ed345
4
+ data.tar.gz: 90512a69c5fba64f7b9076335e5014b292d43e2c
5
+ SHA512:
6
+ metadata.gz: 7bbd1156a220de74b2131579141f193dcb405f4de9c293768f32b822cc75bf1d6ce934af67459297ace932de3cd949537eec5d9d5526d2e85a14d08126af7dcc
7
+ data.tar.gz: 9cdc32f3ba8a84db4573e3e5fd3253061a7ce6c8d83c5bddcd0ef9b56e1ecbcab389ffb6a1f9bf1d1d6a4f1590285dc9306ad91a4fd75419fd36680711ad3902
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in app_earnings.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Egghead Games LLC
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,71 @@
1
+ app_earnings
2
+ ============
3
+
4
+ Process App Store monthly "raw" CSV files into a report by Application and IAP.
5
+ Currently supports Google Play and Amazon Android marketplace data.
6
+
7
+ ## Background
8
+
9
+ Egghead Games is an independent app developer who has to provide monthly royalties to various licensors based on sales.
10
+ None of the existing tools like Distimo or App Annie provided data that was sufficiently accurate to make payments.
11
+ The stores provide data files (usually CSV) with the raw data, but processing these by hand or spreadsheet is painful.
12
+ This command utility takes the CSV files and outputs sales by app and in-app purchase within the app.
13
+ It takes into account refunds and currency conversions, so that the amounts given should exactly correspond to the payments
14
+ made by the app store.
15
+
16
+ ## Description
17
+
18
+ The Google Play and Amazon Android Marketplaces provide monthly earning reports in the form of CSV files.
19
+
20
+ This command line utility processes the transactions, groups them by app and in-app purchase (IAP), and produces the net dollar amount for each app. It includes the following features:
21
+
22
+ * handles returns
23
+ * provides sub-totals for IAPs, so you can see how much individual IAPs are earning.
24
+ * uses Amazon exchange rates to ensure that all amounts are in your final currency
25
+ * should handle non-USD Google Wallet accounts (though this is untested)
26
+ * JSON output is available, to make further processing simpler
27
+
28
+ By default, it provides simple text output.
29
+
30
+ ## Installation
31
+
32
+ Install it yourself as:
33
+
34
+ $ gem install app_earnings
35
+
36
+ ## Usage
37
+
38
+ This will show the full set of current commands and options:
39
+
40
+ app_earnings help
41
+
42
+ ### Google Play Marketplace
43
+
44
+ To process a Google Play monthly earnings report csv file into a text sales report:
45
+
46
+ app_earnings play PlayApps_201401.csv
47
+
48
+ Alternatively, you can get a JSON version of the data with:
49
+
50
+ app_earnings play PlayApps_201401.csv --format json
51
+
52
+ ### Amazon Android Marketplace
53
+
54
+ To process Amazon monthly earnings report files into a text sales report:
55
+
56
+ app_earnings amazon EarningsReport-Jan-1-2014-Jan-31-2014.csv PaymentReport-Feb-1-2014-Mar-1-2014.csv
57
+
58
+ Note that the payment report must be for the month after, as Amazon payments happen a month in arrears.
59
+
60
+ Alternatively, you can get a JSON version of the data with:
61
+
62
+ app_earnings amazon EarningsReport-Jan-1-2014-Jan-31-2014.csv PaymentReport-Feb-1-2014-Mar-1-2014.csv --format json
63
+
64
+
65
+ ## Contributing
66
+
67
+ 1. Fork it ( http://github.com/eggheadgames/app_earnings/fork )
68
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
69
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
70
+ 4. Push to the branch (`git push origin my-new-feature`)
71
+ 5. Create new Pull Request
@@ -0,0 +1,24 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rubocop/rake_task'
3
+ require 'ruby-lint/rake_task'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new
7
+
8
+ task :default => :spec
9
+ task :test => :spec
10
+
11
+ desc 'Run RuboCop on the lib directory'
12
+ Rubocop::RakeTask.new(:rubocop) do |task|
13
+ task.patterns = ['lib/**/*.rb']
14
+ # don't abort rake on failure
15
+ task.fail_on_error = false
16
+ end
17
+
18
+ RubyLint::RakeTask.new do |task|
19
+ task.name = 'lint'
20
+ task.files = ['lib']
21
+ end
22
+
23
+ desc 'Runs specs/Lint/Style checks'
24
+ task :all => [ :lint, :rubocop, :spec ]
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "app_earnings"
7
+ spec.version = "1.0.0"
8
+ spec.authors = ["Egghead Games", "André Luis Leal Cardoso Junior", "Michael Mee"]
9
+ spec.email = ["support@eggheadgames.com"]
10
+ spec.summary = %q{Process app store csv report files into a text/json summary by application and iap}
11
+ spec.description = <<-EOF
12
+ Allows easy calculation of revenue sharing by app and in-app purchases.
13
+ Munges the monthly CSV reports from Google Play or Amazon Android app stores.
14
+ Does Amazon currency conversions and verifies against payment amounts.
15
+ EOF
16
+ spec.homepage = "http://github.com/eggheadgames/app_earnings"
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0")
20
+ spec.executables << 'app_earnings'
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "thor", "~> 0.18.0"
25
+ spec.add_dependency "currencies", "~> 0.4.2"
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.5"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "rubocop", "~> 0.18.1"
30
+ spec.add_development_dependency "ruby-lint", "~> 1.1.0"
31
+ spec.add_development_dependency "rspec", "~> 2.14"
32
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ begin
6
+ require 'app_earnings'
7
+ rescue LoadError
8
+ lib = File.expand_path('../../lib', __FILE__)
9
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
10
+
11
+ require 'app_earnings'
12
+ end
13
+
14
+ AppEarnings::Cli.start(ARGV)
@@ -0,0 +1,21 @@
1
+ require 'app_earnings/cli'
2
+ require 'app_earnings/report'
3
+ require 'app_earnings/amazon'
4
+ require 'app_earnings/google_play'
5
+
6
+ # Process Monthly Earnings report
7
+ # From GoogleApps or Amazon
8
+ # transaction CSV file into a report by Application and IAP
9
+ module AppEarnings
10
+ def self.play_report(name, format = 'text')
11
+ parsed = GooglePlay::Parser.new(name).extract
12
+ GooglePlay::Reporter.new(parsed).report_as(format)
13
+ end
14
+
15
+ def self.amazon_report(payments, earnings, format = 'text')
16
+ parsed = []
17
+ parsed << Amazon::Parser.new(payments).extract
18
+ parsed << Amazon::Parser.new(earnings).extract
19
+ Amazon::Reporter.new(parsed).report_as(format)
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ require 'app_earnings/amazon/parser'
2
+ require 'app_earnings/amazon/amazon_report'
3
+ require 'app_earnings/amazon/reporter'
@@ -0,0 +1,37 @@
1
+ require 'yaml'
2
+ require 'iso4217'
3
+
4
+ module AppEarnings::Amazon
5
+ # Converts a csv file to a hash.
6
+ class AmazonReport
7
+ include AppEarnings::Report
8
+
9
+ attr_accessor :exchange_info
10
+
11
+ def initialize(name, transactions, exchange_info)
12
+ @exchange_info = exchange_info
13
+ extract_amount(name, transactions)
14
+ end
15
+
16
+ def convert_amounts(amounts)
17
+ amounts.reduce(0.0) do |sum, (marketplace, amount)|
18
+ amount = amount * @exchange_info[marketplace].to_f
19
+ sum + amount
20
+ end
21
+ end
22
+
23
+ def amount_from_transactions(transactions)
24
+ amounts = transactions.reduce({}) do |sum, transaction|
25
+ marketplace = transaction[:marketplace]
26
+ sum[marketplace] ||= 0.0
27
+ sum[marketplace] += transaction[:gross_earnings_or_refunds].to_f
28
+ sum
29
+ end
30
+
31
+ {
32
+ currency: 'USD',
33
+ amount: convert_amounts(amounts).round(2)
34
+ }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,42 @@
1
+ require 'csv'
2
+
3
+ module AppEarnings::Amazon
4
+ # Converts a csv file to a hash.
5
+ class Parser
6
+ attr_accessor :file_name
7
+
8
+ def initialize(file_name)
9
+ @file_name = file_name
10
+ @contents = File.read(@file_name)
11
+ @header, @summary, @details = @contents.split(/Summary|Detail/)
12
+
13
+ if @header =~ /Payment Report/
14
+ @report_type = :payments
15
+ else
16
+ @report_type = :earnings
17
+ end
18
+ end
19
+
20
+ def extract
21
+ {
22
+ report_type: @report_type,
23
+ header: @header,
24
+ summary: parse(@summary.strip),
25
+ details: parse((@details || '').strip)
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ def parse(content)
32
+ return nil if content.nil?
33
+ extracted_data = []
34
+ options = { headers: true, header_converters: :symbol }
35
+
36
+ CSV.parse(content, options) do |row|
37
+ extracted_data << row.to_hash
38
+ end
39
+ extracted_data
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,89 @@
1
+ module AppEarnings::Amazon
2
+ # Generates a report based on the data provided
3
+ class Reporter
4
+ AVAILABLE_FORMATS = %w(json text)
5
+ attr_accessor :data
6
+
7
+ def initialize(data)
8
+ @data = data
9
+ @payments_data = @data.find { |r| r[:report_type] == :payments }
10
+ @earnings_data = (@data - [@payments_data]).first
11
+ @exchange_info = fetch_exchange_info
12
+ end
13
+
14
+ def fetch_exchange_info
15
+ @payments_full_amount = 0.0
16
+ @payments_data[:summary].reduce({}) do |all_info, data|
17
+ all_info[data[:marketplace]] = data[:fx_rate]
18
+ @payments_full_amount += data[:total_payment].gsub(/,/, '').to_f
19
+ all_info
20
+ end
21
+ end
22
+
23
+ def full_amount
24
+ total = @reports.reduce(0.0) { |a, e| a + e.amount.to_f }
25
+ total - refunds
26
+ end
27
+
28
+ def refunds
29
+ @earnings_data[:summary].reduce(0.0) do |sum, marketplace|
30
+ currency = marketplace[:refunds_currency_code]
31
+ amount = marketplace[:refunds].gsub(/\(|\)/, '').to_f
32
+ amount = amount * @exchange_info[currency].to_f if currency != 'USD'
33
+ sum + amount
34
+ end
35
+ end
36
+
37
+ def generate
38
+ @reports = []
39
+ @data.each do |raw_data|
40
+ if raw_data[:report_type] == :earnings
41
+ by_apps = raw_data[:details].group_by { |element| element[:app] }
42
+ .sort_by { |app| app }
43
+
44
+ by_apps.each do |key, application|
45
+ @reports << AmazonReport.new(key, application, @exchange_info)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def report_as(format = 'text')
52
+ unless AVAILABLE_FORMATS.include?(format)
53
+ fail "#{format} Not supported. Available formats are: " +
54
+ " #{AVAILABLE_FORMATS.join(", ")}"
55
+ end
56
+
57
+ generate
58
+ render_as(format)
59
+ end
60
+
61
+ def render_as(format = 'text')
62
+ case format
63
+ when 'text'
64
+ as_text
65
+ when 'json'
66
+ as_json
67
+ end
68
+ end
69
+
70
+ def as_text
71
+ amount = AppEarnings::Report.formatted_amount('USD', full_amount)
72
+ refund = AppEarnings::Report.formatted_amount('USD', refunds)
73
+ puts @reports
74
+ puts "Total of refunds: #{refund}"
75
+ puts "Total of all transactions: #{amount}"
76
+
77
+ if @payments_full_amount.round(2) != full_amount
78
+ puts "Total from Payment Report: #{@payments_full_amount.round(2)}"
79
+ end
80
+ @reports
81
+ end
82
+
83
+ def as_json
84
+ puts JSON.generate(apps: @reports.map(&:to_json),
85
+ currency: 'USD',
86
+ total: full_amount)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,31 @@
1
+ require 'thor'
2
+
3
+ module AppEarnings
4
+ # Command-line utility: Retrieves CSV file
5
+ # and generates a report based into it.
6
+ class Cli < Thor
7
+ desc 'play PlayApps.csv', 'Generates a detailed report'\
8
+ ' for the provided Google Play transaction file'
9
+ method_option :format, type: :string, default: 'text',
10
+ aliases: '-f', enum: %w(text json)
11
+ def play(name)
12
+ if File.exists?(name)
13
+ AppEarnings.play_report(name, options[:format])
14
+ else
15
+ puts "File '#{name}' not found!"
16
+ end
17
+ end
18
+
19
+ desc 'amazon PaymentReport.csv EarningsReport.csv', 'Generates a '\
20
+ 'detailed report for the Amazon report files'
21
+ method_option :format, type: :string, default: 'text',
22
+ aliases: '-f', enum: %w(text json)
23
+ def amazon(payments, earnings)
24
+ if File.exists?(payments) && File.exists?(earnings)
25
+ AppEarnings.amazon_report(payments, earnings, options[:format])
26
+ else
27
+ puts "Files '#{payments}' and '#{earnings}' were not found!"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ require 'app_earnings/google_play/parser'
2
+ require 'app_earnings/google_play/play_report'
3
+ require 'app_earnings/google_play/reporter'
@@ -0,0 +1,22 @@
1
+ require 'csv'
2
+
3
+ module AppEarnings::GooglePlay
4
+ # Converts a csv file to a hash.
5
+ class Parser
6
+ attr_accessor :file_name
7
+
8
+ def initialize(file_name)
9
+ @file_name = file_name
10
+ end
11
+
12
+ def extract
13
+ @extracted_data = []
14
+ options = { headers: true, header_converters: :symbol }
15
+
16
+ CSV.foreach(@file_name, options) do |row|
17
+ @extracted_data << row.to_hash
18
+ end
19
+ @extracted_data
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ module AppEarnings::GooglePlay
2
+ # Represents the report.
3
+ class PlayReport
4
+ include AppEarnings::Report
5
+
6
+ def initialize(name, transactions)
7
+ extract_amount(name, transactions)
8
+ unless transactions_from_product.first.nil?
9
+ @description = transactions_from_product.first[:product_title]
10
+ end
11
+ end
12
+
13
+ # It sums up to all available amounts, but it takes just the first one.
14
+ # As it's usually just one.
15
+ def amount_from_transactions(transactions)
16
+ all_currencies = transactions.reduce({}) do |sum, transaction|
17
+ currency = transaction[:merchant_currency]
18
+ sum[currency] ||= 0.0
19
+ sum[currency] += transaction[:amount_merchant_currency].to_f
20
+ sum
21
+ end.first
22
+
23
+ {
24
+ currency: all_currencies.first,
25
+ amount: all_currencies.last.round(2)
26
+ }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,65 @@
1
+ require 'yaml'
2
+ require 'json'
3
+ require 'iso4217'
4
+
5
+ module AppEarnings::GooglePlay
6
+ # Generates a report based on the data provided
7
+ class Reporter
8
+ AVAILABLE_FORMATS = %w(json text)
9
+ attr_accessor :raw_data
10
+
11
+ def initialize(raw_data)
12
+ @raw_data = raw_data
13
+ end
14
+
15
+ def generate
16
+ by_apps = @raw_data.group_by { |element| element[:product_id] }
17
+ .sort_by { |app| app }
18
+
19
+ @reports = []
20
+ by_apps.each do |key, application|
21
+ @reports << PlayReport.new(key, application)
22
+ end
23
+ end
24
+
25
+ def full_amount
26
+ total = @reports.reduce(0.0) { |a, e| a + e.amount.to_f }
27
+ currency = @reports.first.currency
28
+ [currency, total]
29
+ end
30
+
31
+ def report_as(format = 'text')
32
+ unless AVAILABLE_FORMATS.include?(format)
33
+ fail "#{format} Not supported. Available formats are: " +
34
+ " #{AVAILABLE_FORMATS.join(", ")}"
35
+ end
36
+
37
+ generate
38
+ render_as(format)
39
+ end
40
+
41
+ def render_as(format = 'text')
42
+ case format
43
+ when 'text'
44
+ as_text
45
+ when 'json'
46
+ as_json
47
+ end
48
+ end
49
+
50
+ def as_text
51
+ currency, total = full_amount
52
+ formatted_amount = AppEarnings::Report.formatted_amount(currency, total)
53
+ puts @reports
54
+ puts "Total of all transactions: #{formatted_amount}"
55
+ @reports
56
+ end
57
+
58
+ def as_json
59
+ currency, total = full_amount
60
+ puts JSON.generate(apps: @reports.map(&:to_json),
61
+ currency: currency,
62
+ total: total)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,94 @@
1
+ module AppEarnings
2
+ # Base class for reports
3
+ module Report
4
+ attr_accessor :name, :transactions, :amount, :currency
5
+
6
+ def extract_amount(name, transactions)
7
+ @name = name
8
+ @transactions = transactions
9
+ @total_amount = amount_from_transactions(@transactions)
10
+ @currency = @total_amount[:currency]
11
+ @amount = @total_amount[:amount]
12
+ end
13
+
14
+ def transactions_by_type
15
+ transactions.group_by { |transaction| transaction[:transaction_type] }
16
+ end
17
+
18
+ def transactions_from_in_app_purchases
19
+ transactions.reject do |tr|
20
+ tr[:sku_id].nil? && tr[:vendor_sku].nil?
21
+ end
22
+ end
23
+
24
+ def transactions_from_in_app_purchases_by_id_and_name
25
+ transactions_from_in_app_purchases.group_by do |tr|
26
+ [tr[:sku_id] || tr[:vendor_sku],
27
+ tr[:item_name] || tr[:product_title]]
28
+ end
29
+ end
30
+
31
+ def transactions_count_by_type
32
+ transaction_count = {}
33
+ transactions_by_type.each do |type, transactions|
34
+ transaction_count[type] = transactions.length
35
+ end
36
+ transaction_count
37
+ end
38
+
39
+ def transactions_from_product
40
+ transactions - transactions_from_in_app_purchases
41
+ end
42
+
43
+ def total_from_in_app_purchases
44
+ transactions_from_in_app_purchases_by_id_and_name.map do |iap, tr|
45
+ {
46
+ id: iap.first,
47
+ name: iap.last
48
+ }.merge(amount_from_transactions(tr))
49
+ end
50
+ end
51
+
52
+ def formatted_transactions_count_by_type
53
+ transactions_count_by_type.sort_by { |name, _| name }.map do |tr|
54
+ tr.join(': ')
55
+ end
56
+ end
57
+
58
+ def formatted_total_by_products
59
+ total_from_in_app_purchases.sort_by { |product| product[:id] }
60
+ .map do |app|
61
+ total_amount = Report.formatted_amount(app[:currency], app[:amount])
62
+ "#{app[:id]} - #{app[:name]}: #{total_amount}"
63
+ end
64
+ end
65
+
66
+ def self.formatted_amount(currency, amount)
67
+ symbol = ISO4217::Currency.from_code(currency).symbol
68
+ "#{currency} #{symbol}#{sprintf('%.2f', amount)}"
69
+ end
70
+
71
+ def to_json
72
+ {
73
+ id: @name,
74
+ name: @description,
75
+ transactions_types: transactions_count_by_type,
76
+ total: @amount.round(2),
77
+ currency: @currency,
78
+ subtotals: total_from_in_app_purchases
79
+ }
80
+ end
81
+
82
+ def to_s
83
+ %Q(#{@name} #{@description}
84
+ Transactions: #{formatted_transactions_count_by_type.join(", ")}
85
+ Total:
86
+ #{Report.formatted_amount(@currency, @amount)}
87
+
88
+ Sub totals by IAP:
89
+ #{formatted_total_by_products.join("\n")}
90
+
91
+ )
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe AppEarnings::Amazon::Reporter do
4
+ let(:payments_data) { AppEarnings::Amazon::Parser.new(file("amazon_payments.csv")).extract }
5
+ let(:earnings_data) { AppEarnings::Amazon::Parser.new(file("amazon_earnings.csv")).extract }
6
+ let(:dummy_data) { [ { merchant_currency: "USD", amount_merchant_currency: 10.0 },
7
+ { merchant_currency: "USD", amount_merchant_currency: 5.0 } ] }
8
+ let(:reporter) { AppEarnings::Amazon::Reporter.new([payments_data, earnings_data]) }
9
+
10
+ it "should return an array of report objects" do
11
+ data = reporter.report_as('text')
12
+ expect(data).to be_a(Array)
13
+ expect(data.first).to be_a(AppEarnings::Amazon::AmazonReport)
14
+ end
15
+
16
+ it "should fail when trying to use an unsupported format" do
17
+ expect { reporter.report_as('unknown') }.to raise_error
18
+ end
19
+
20
+ it "should sum refunds" do
21
+ reporter.refunds.should eql(4.18)
22
+ end
23
+
24
+ it "should return total amount" do
25
+ reporter.generate
26
+ reporter.full_amount.should eql(10.3)
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe AppEarnings::Amazon::Parser do
4
+ context "Validation" do
5
+ it "should fail when an invalid csv is provided" do
6
+ expect { AppEarnings::Amazon::Parser.new("non_existant.csv").extract }.to raise_error(Errno::ENOENT)
7
+ end
8
+ end
9
+
10
+ context "Extracted data" do
11
+ let(:extracted_data_earnings) { AppEarnings::Amazon::Parser.new(file("amazon_earnings.csv")).extract }
12
+ let(:extracted_data_payments) { AppEarnings::Amazon::Parser.new(file("amazon_payments.csv")).extract }
13
+
14
+ it "should retrieve data from the provided csv and find out the type" do
15
+ expect(extracted_data_earnings[:report_type]).to eql(:earnings)
16
+ expect(extracted_data_earnings).to_not be_empty
17
+ end
18
+
19
+ it "should retrieve data from the provided csv and find out the type" do
20
+ expect(extracted_data_payments[:report_type]).to eql(:payments)
21
+ expect(extracted_data_payments).to_not be_empty
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe AppEarnings do
4
+ it "should generate a report by application based on a provided csv file for googple play" do
5
+ expect_any_instance_of(AppEarnings::GooglePlay::Parser).to receive(:extract)
6
+ expect_any_instance_of(AppEarnings::GooglePlay::Reporter).to receive(:report_as).with('text')
7
+
8
+ AppEarnings.play_report(file("play_transactions.csv"))
9
+ end
10
+
11
+ it "should generate a report by application based on a provided csv file for amazon" do
12
+ expect_any_instance_of(AppEarnings::Amazon::Reporter).to receive(:report_as).with('text')
13
+ AppEarnings.amazon_report(file("amazon_earnings.csv"), file("amazon_payments.csv"))
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe AppEarnings::GooglePlay::Parser do
4
+ context "Validation" do
5
+ it "should fail when an invalid csv is provided" do
6
+ expect { AppEarnings::GooglePlay::Parser.new("non_existant.csv").extract }.to raise_error(Errno::ENOENT)
7
+ end
8
+ end
9
+
10
+ context "Extracted data" do
11
+ let(:extracted_data) { AppEarnings::GooglePlay::Parser.new(file("play_transactions.csv")).extract }
12
+
13
+ it "should retrieve data from the provided csv" do
14
+ expect(extracted_data).to_not be_empty
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe AppEarnings::GooglePlay::Reporter do
4
+ let(:extracted_data) { AppEarnings::GooglePlay::Parser.new(file("play_transactions.csv")).extract }
5
+ let(:dummy_data) { [ { merchant_currency: "USD", amount_merchant_currency: 10.0 },
6
+ { merchant_currency: "USD", amount_merchant_currency: 5.0 } ] }
7
+ let(:reporter) { AppEarnings::GooglePlay::Reporter.new(extracted_data) }
8
+
9
+ it "should return an array of report objects" do
10
+ data = reporter.report_as('text')
11
+ expect(data).to be_a(Array)
12
+ expect(data.first).to be_a(AppEarnings::GooglePlay::PlayReport)
13
+ end
14
+
15
+ it "should fail when trying to use an unsupported format" do
16
+ expect { reporter.report_as('unknown') }.to raise_error
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe AppEarnings::Report do
4
+ let(:google_fee) { { product_title: "one", transaction_type: "Google Fee", merchant_currency: "USD", amount_merchant_currency: 10.0, sku_id: '1' } }
5
+ let(:charge) { { product_title: "two", transaction_type: "Charge", merchant_currency: "USD", amount_merchant_currency: 20.0 } }
6
+ let(:charge_eur) { { product_title: "two", transaction_type: "Charge", merchant_currency: "EUR", amount_merchant_currency: 20.0 } }
7
+ let(:transactions) { [ google_fee, charge, google_fee, charge_eur ] }
8
+
9
+ before(:each) do
10
+ @report = AppEarnings::GooglePlay::PlayReport.new("Product one", transactions)
11
+ end
12
+
13
+ it "should return count of transactions by type" do
14
+ expect(@report.transactions_count_by_type).to include( { "Google Fee" => 2 }, { "Charge" => 2 })
15
+ end
16
+
17
+ it "should group transactions by type" do
18
+ expect(@report.transactions_by_type.keys).to include("Google Fee", "Charge")
19
+ end
20
+
21
+ it "should not include transactions made for IAP" do
22
+ expect(@report.transactions_from_product.all? { |tr| tr[:sku_id].nil? }).to be_true
23
+ end
24
+
25
+ it "should not include transactions made from product" do
26
+ expect(@report.transactions_from_in_app_purchases.all? { |tr| !tr[:sku_id].nil? }).to be_true
27
+ end
28
+
29
+ it "should group transactions by product title (IAP)" do
30
+ transactions_by_product = @report.transactions_from_in_app_purchases_by_id_and_name
31
+ counter = transactions_by_product.map { |product, transactions| [ product, transactions.length] }
32
+ expect(counter).to include([["1", "one"], 2])
33
+ end
34
+
35
+ it "should retrieve amount from a list of transactions" do
36
+ amount = @report.amount_from_transactions(transactions)
37
+ expect(amount).to include({ currency: "USD", amount: 40.00 })
38
+ end
39
+
40
+ it "should retrieve totals by product title" do
41
+ amounts = @report.total_from_in_app_purchases
42
+ expect(amounts).to include({ id: "1", name: "one", currency: "USD", amount: 20.00})
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ Amazon Mobile App Distribution
2
+ Earnings Report
3
+ 1/1/2014 - 1/31/2014
4
+ Mobile Apps
5
+
6
+ Notes
7
+ Gross Earnings - Gross royalties and/or commissions earned by the developer during the period.
8
+ Refunds - The royalty and/or commission portion of the total amount refunded to customers who returned their purchase.
9
+ "Adjustments - Sum of all adjustments that occurred on your account during the specified month, such as pricing adjustments for marketing promotions. Please contact us with questions about a specific adjustment."
10
+ Earnings Before Tax - May not reflect the actual disbursement values when royalties are subject to a withholding or payment thresholds have not been met. Please see the FAQ on the developer portal or your developer agreement for more detail.
11
+
12
+ Summary
13
+ Marketplace,From Date,To Date,Charge Units,Refund Units,Gross Earnings,Gross Earnings Currency Code,Refunds,Refunds Currency Code,Adjustments,Adjustments Currency Code,Earnings Before Tax,Earnings Before Tax Currency Code
14
+ Amazon.ca,1/1/2014,1/31/2014,7,0,2.09,CAD,0.00,CAD,0.00,CAD,1.00,CAD
15
+ Amazon.cn,1/1/2014,1/31/2014,0,0,0.00,CNY,0.00,CNY,0.00,CNY,0.00,CNY
16
+ Amazon.co.jp,1/1/2014,1/31/2014,0,0,0,JPY,0,JPY,0,JPY,0,JPY
17
+ Amazon.co.uk,1/1/2014,1/31/2014,244,0,73.61,GBP,0.00,GBP,0.00,GBP,20.00,GBP
18
+ Amazon.com,1/1/2014,1/31/2014,"3,121",-2,"2,146.67",USD,(4.18),USD,0.00,USD,"2,000.12",USD
19
+ Amazon.com.au,1/1/2014,1/31/2014,0,0,0.00,AUD,0.00,AUD,0.00,AUD,0.00,AUD
20
+ Amazon.com.br,1/1/2014,1/31/2014,0,0,0.00,BRL,0.00,BRL,0.00,BRL,0.00,BRL
21
+ Amazon.de,1/1/2014,1/31/2014,0,0,0.00,EUR,0.00,EUR,0.00,EUR,0.00,EUR
22
+ Amazon.es,1/1/2014,1/31/2014,0,0,0.00,EUR,0.00,EUR,0.00,EUR,0.00,EUR
23
+ Amazon.fr,1/1/2014,1/31/2014,0,0,0.00,EUR,0.00,EUR,0.00,EUR,0.00,EUR
24
+ Amazon.it,1/1/2014,1/31/2014,0,0,0.00,EUR,0.00,EUR,0.00,EUR,0.00,EUR
25
+
26
+ Detail
27
+ Marketplace,Date,App,Item Name,Period,Vendor SKU,Item Type,Transaction Type,List Price,List Price Currency Code,Sales Price,Sales Price Currency Code,units,Gross Earnings or Refunds,Gross Earnings or Refunds Currency Code
28
+ Amazon.com,1/1/2014,App,,,,Apps,CHARGE,2.99,USD,2.99,USD,4,8.36,USD
29
+ Amazon.co.uk,1/1/2014,Metal Gear,,,,Apps,CHARGE,1.09,GBP,0.51,GBP,1,0.36,GBP
30
+ Amazon.com,1/1/2014,Metal Gear,,,,Apps,CHARGE,1.99,USD,0.99,USD,8,5.52,USD
31
+ Amazon.co.uk,1/1/2014,Metal Gear FREE Trial,,,,Apps,CHARGE,0.00,GBP,0.00,GBP,1,0.00,GBP
32
+ Amazon.com,1/1/2014,Metal Gear FREE Trial,,,,Apps,CHARGE,0.00,USD,0.00,USD,14,0.00,USD
@@ -0,0 +1,18 @@
1
+ Amazon Mobile App Distribution
2
+ Payment Report
3
+ 1/23/2014 - 2/10/2014
4
+ Mobile Apps
5
+
6
+ Notes
7
+ Payment Cost is the cost of disbursing your earnings in your home currency
8
+ Tax Witheld is the taxes Amazon withholds from your earnings due to US tax withholding requirements
9
+ A more detailed calculation of earnings can be found in the Earnings Report.
10
+
11
+ Summary
12
+ Payment Date,Marketplace,Earnings Period,Earnings Before Tax,Earnings Before Tax Currency Code,Tax Withheld,Tax Withheld Currency Code,Payment Cost,Payment Cost Currency Code,Payment Method,FX Rate,Total Payment,Total Payment Currency Code,Payment Comment
13
+ 1/30/2014,Amazon.ca,12/1/2013 - 12/31/2013,2.09,CAD,0.00,CAD,0.00,CAD,EFT,0.894737,1.0,USD,
14
+ 1/30/2014,Amazon.co.uk,12/1/2013 - 12/31/2013,115.24,GBP,0.00,GBP,0.00,GBP,EFT,1.655675,100.10,USD,
15
+ 1/30/2014,Amazon.com,12/1/2013 - 12/31/2013,"2,221.57",USD,0.00,USD,0.00,USD,EFT,1.000000,"2,200.12",USD,
16
+ 1/30/2014,Amazon.com.au,12/1/2013 - 12/31/2013,4.54,AUD,0.00,AUD,0.00,AUD,EFT,0.876652,2.00,USD,
17
+ 1/30/2014,Amazon.de,12/1/2013 - 12/31/2013,1.52,EUR,0.00,EUR,0.00,EUR,EFT,1.361842,2.07,USD,
18
+
@@ -0,0 +1,4 @@
1
+ Description,Transaction Date,Transaction Time,Tax Type,Transaction Type,Refund Type,Product Title,Product id,Product Type,Sku Id,Hardware,Buyer Country,Buyer State,Buyer Postal Code,Buyer Currency,Amount (Buyer Currency),Currency Conversion Rate,Merchant Currency,Amount (Merchant Currency)
2
+ 1376321714625257,"Jan 29, 2014",7:33:12 AM PST,,Charge,,Flappy Bird,com.example.flappy,0,,m0,AU,QLD,4520,AUD,2.99,0.874200,USD,2.61
3
+ 1376321714625257,"Jan 29, 2014",7:33:12 AM PST,,Google fee,,Flappy Bird,com.example.flappy,0,,m0,AU,QLD,4520,AUD,-0.90,0.874200,USD,-0.79
4
+ 1346161764058234,"Jan 29, 2014",8:03:14 AM PST,,Charge,,Flappy Clone,example.clone,0,,espresso10rf,AU,,3165,AUD,2.99,0.875700,USD,2.62
@@ -0,0 +1,17 @@
1
+ require './lib/app_earnings'
2
+
3
+ RSpec.configure do |config|
4
+ config.mock_with :rspec
5
+ config.color_enabled = true
6
+ config.tty = true
7
+
8
+ config.formatter = :documentation # :progress, :html, :textmate
9
+ config.before do
10
+ AppEarnings::Amazon::Reporter.any_instance.stub(:puts)
11
+ AppEarnings::GooglePlay::Reporter.any_instance.stub(:puts)
12
+ end
13
+ end
14
+
15
+ def file(name)
16
+ File.expand_path(File.dirname(__FILE__) + '/fixtures/' + name)
17
+ end
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: app_earnings
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Egghead Games
8
+ - André Luis Leal Cardoso Junior
9
+ - Michael Mee
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2014-04-10 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: thor
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.18.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ version: 0.18.0
29
+ - !ruby/object:Gem::Dependency
30
+ name: currencies
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ~>
34
+ - !ruby/object:Gem::Version
35
+ version: 0.4.2
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ version: 0.4.2
43
+ - !ruby/object:Gem::Dependency
44
+ name: bundler
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ~>
48
+ - !ruby/object:Gem::Version
49
+ version: '1.5'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ~>
55
+ - !ruby/object:Gem::Version
56
+ version: '1.5'
57
+ - !ruby/object:Gem::Dependency
58
+ name: rake
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ - !ruby/object:Gem::Dependency
72
+ name: rubocop
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 0.18.1
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ~>
83
+ - !ruby/object:Gem::Version
84
+ version: 0.18.1
85
+ - !ruby/object:Gem::Dependency
86
+ name: ruby-lint
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ~>
90
+ - !ruby/object:Gem::Version
91
+ version: 1.1.0
92
+ type: :development
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ~>
97
+ - !ruby/object:Gem::Version
98
+ version: 1.1.0
99
+ - !ruby/object:Gem::Dependency
100
+ name: rspec
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ~>
104
+ - !ruby/object:Gem::Version
105
+ version: '2.14'
106
+ type: :development
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ~>
111
+ - !ruby/object:Gem::Version
112
+ version: '2.14'
113
+ description: "Allows easy calculation of revenue sharing by app and in-app purchases.
114
+ \nMunges the monthly CSV reports from Google Play or Amazon Android app stores.
115
+ \nDoes Amazon currency conversions and verifies against payment amounts.\n"
116
+ email:
117
+ - support@eggheadgames.com
118
+ executables:
119
+ - app_earnings
120
+ extensions: []
121
+ extra_rdoc_files: []
122
+ files:
123
+ - .gitignore
124
+ - Gemfile
125
+ - LICENSE.txt
126
+ - README.md
127
+ - Rakefile
128
+ - app_earnings.gemspec
129
+ - lib/app_earnings.rb
130
+ - lib/app_earnings/amazon.rb
131
+ - lib/app_earnings/amazon/amazon_report.rb
132
+ - lib/app_earnings/amazon/parser.rb
133
+ - lib/app_earnings/amazon/reporter.rb
134
+ - lib/app_earnings/cli.rb
135
+ - lib/app_earnings/google_play.rb
136
+ - lib/app_earnings/google_play/parser.rb
137
+ - lib/app_earnings/google_play/play_report.rb
138
+ - lib/app_earnings/google_play/reporter.rb
139
+ - lib/app_earnings/report.rb
140
+ - spec/app_earnings/amazon/amazon_reporter_spec.rb
141
+ - spec/app_earnings/amazon/parser_spec.rb
142
+ - spec/app_earnings/app_earnings_spec.rb
143
+ - spec/app_earnings/google_play/parser_spec.rb
144
+ - spec/app_earnings/google_play/reporter_spec.rb
145
+ - spec/app_earnings/report_spec.rb
146
+ - spec/fixtures/amazon_earnings.csv
147
+ - spec/fixtures/amazon_payments.csv
148
+ - spec/fixtures/play_transactions.csv
149
+ - spec/spec_helper.rb
150
+ - bin/app_earnings
151
+ homepage: http://github.com/eggheadgames/app_earnings
152
+ licenses:
153
+ - MIT
154
+ metadata: {}
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - '>='
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - '>='
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubyforge_project:
171
+ rubygems_version: 2.0.3
172
+ signing_key:
173
+ specification_version: 4
174
+ summary: Process app store csv report files into a text/json summary by application
175
+ and iap
176
+ test_files:
177
+ - spec/app_earnings/amazon/amazon_reporter_spec.rb
178
+ - spec/app_earnings/amazon/parser_spec.rb
179
+ - spec/app_earnings/app_earnings_spec.rb
180
+ - spec/app_earnings/google_play/parser_spec.rb
181
+ - spec/app_earnings/google_play/reporter_spec.rb
182
+ - spec/app_earnings/report_spec.rb
183
+ - spec/fixtures/amazon_earnings.csv
184
+ - spec/fixtures/amazon_payments.csv
185
+ - spec/fixtures/play_transactions.csv
186
+ - spec/spec_helper.rb
187
+ has_rdoc: