app_earnings 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: