sales 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. data/Autoingestion.class +0 -0
  2. data/README.md +19 -0
  3. data/Rakefile +22 -0
  4. data/VERSION.yml +5 -0
  5. data/bin/sale +421 -0
  6. data/sales.yml +4 -0
  7. metadata +59 -0
Binary file
data/README.md ADDED
@@ -0,0 +1,19 @@
1
+ sales
2
+ =====
3
+
4
+ iTunes Connect Command Line Autoingestion Script. Besides downloading, also computes and presents totals.
5
+
6
+ usage
7
+ =====
8
+
9
+ in the directory where You want to download Your iTunes Connect reports, run _sale_.
10
+ this will copy a file called _sales.yml_ into that directory.
11
+ open _sales.yml_ and fill in Your iTunes Connect credentials:
12
+
13
+ :username: kitschmaster@gmail.com #iTunes connect username
14
+ :password: yourpassword #iTunes Connect password
15
+ :vendorId: 80076733 #iTunes Connect -> Sales and Trends, find the vendorId on the header of the table next to the company name
16
+
17
+ with the credentials in place run _sale_ again, it should now download the latest daily report and present it.
18
+
19
+ run `sale help` to get a list of all possible commands.
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gem|
6
+ gem.name = "sales"
7
+ gem.summary = %Q{iTunes Connect Command Line Autoingestion Script.}
8
+ gem.email = "kitschmaster@gmail.com"
9
+ gem.homepage = "http://github.com/mihael/sales"
10
+ gem.authors = ["Mihael"]
11
+ gem.rubyforge_project = "Sales"
12
+ gem.description = %Q{iTunes Connect Command Line Autoingestion Script. Computes and presents totals. Uses Autoingestion.class for report downloading.}
13
+ gem.files = FileList['bin/*', '[A-Z]*', 'sales.yml'].to_a # 'lib/**/*.*', 'test/**/*'
14
+ gem.executables = ['sale']
15
+ gem.default_executable = 'sale'
16
+ #gem.add_dependency('haml')
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ #Jeweler::RubyforgeTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
22
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 0
4
+ :patch: 1
5
+ :build:
data/bin/sale ADDED
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Sales
4
+
5
+ #make nice colors for terminal strings
6
+ class String
7
+ def red; colorize(self, "\e[1m\e[31m"); end
8
+ def green; colorize(self, "\e[1m\e[32m"); end
9
+ def dark_green; colorize(self, "\e[32m"); end
10
+ def yellow; colorize(self, "\e[1m\e[33m"); end
11
+ def blue; colorize(self, "\e[1m\e[34m"); end
12
+ def dark_blue; colorize(self, "\e[34m"); end
13
+ def pur; colorize(self, "\e[1m\e[35m"); end
14
+ def colorize(text, color_code) "#{color_code}#{text}\e[0m" end
15
+ def clean; self.gsub(/\n|\t|\r/, ' ').gsub(/[\(\)\/_-]/, ' ').squeeze(' ').strip end
16
+ end
17
+
18
+ #the usage of this program
19
+ USAGE = <<-EGASU
20
+
21
+ getting daily reports for the last 14 days:
22
+ #{'sale get:daily'.green}
23
+
24
+ getting daily reports for the last 90 days:
25
+ #{'sale get:weekly'.green}
26
+
27
+ compute alltime reports
28
+ #{'sale daily'.green}
29
+ #{'sale weekly'.green}
30
+
31
+ get and present the last daily summary report
32
+ #{'sale'}.green
33
+
34
+ get and present a daily summary report for a date
35
+ #{'sale.rb YYYYMMDD'.green}
36
+
37
+ EGASU
38
+
39
+ require 'rubygems'
40
+ require 'open-uri'
41
+ require 'json'
42
+ require 'date'
43
+ require 'yaml'
44
+
45
+ V = YAML.load_file(File.join(File.dirname(__FILE__), %w[.. VERSION.yml]))
46
+ VERSION = "#{V[:major]}.#{V[:minor]}.#{V[:patch]}"
47
+
48
+ #load the iTunes Connect credentials from the sales.yml
49
+ example_sales_yml_file = File.join(File.dirname(__FILE__), %w[.. sales.yml])
50
+ dest_sales_yml_file = "sales.yml"
51
+ if File.exists? dest_sales_yml_file
52
+ S = YAML.load_file("sales.yml")
53
+ username = S[:username]
54
+ password = S[:password]
55
+ vendorId = S[:vendorId]
56
+ else
57
+ puts "Please fill out Your iTunes Connect credentials in the".red + " sales.yml ".green + "file in the same dir where You run 'sale'.".red
58
+ `cp #{example_sales_yml_file} sales.yml; ls -al` unless File.exists? dest_sales_yml_file
59
+ abort
60
+ end
61
+
62
+ #prepare the java runtime classpath for the Autoingestion.class
63
+ classpath = File.join(File.dirname(__FILE__), %w[..])
64
+
65
+ #identifiers
66
+ PRODUCT_TYPE_IDENTIFIER = {
67
+ "1" => "Free or Paid Apps, iPhone and iPod Touch",
68
+ "7" => "Updates, iPhone and iPod Touch",
69
+ "IA1" => "In Apps Purchase",
70
+ "IA9" => "In Apps Subscription",
71
+ "IAY" => "Auto-Renewable Subscription",
72
+ "1F" => "Free or Paid Apps (Universal)",
73
+ "7F" => "Updates (Universal)",
74
+ "1T" => "Free or Paid Apps, iPad",
75
+ "7T" => "Updates, iPad",
76
+ "F1" => "Free or Paid Apps, Mac OS",
77
+ "F7" => "Updates, Mac OS",
78
+ "FI1" => "In Apps Purchase, Mac OS",
79
+ "1E" => "Custome iPhone and iPod Touch",
80
+ "1EP" => "Custome iPad",
81
+ "1EU" => "Custome Universal"
82
+ }
83
+ SALE_IDENTS = ["1", "1F", "1T", "F1"]
84
+ INAPP_SALE_IDENTS = ["IA1", "IA9", "IAY", "FI1"]
85
+ UPDATE_IDENTS = ["7", "7F", "7T", "F7"]
86
+
87
+ #the parser was copied from github, https://github.com/siuying/itunes-auto-ingestion/blob/master/lib/itunes_ingestion/sales_report_parser.rb
88
+ class SalesReportParser
89
+ # Parse sales report
90
+ #
91
+ # report - text based report form itunesconnect
92
+ #
93
+ # Returns array of hash, each hash contains one line of sales report
94
+ def self.parse(report)
95
+ lines = report.split("\n")
96
+ #puts "lines: #{lines}"
97
+ header = lines.shift # remove first line
98
+ lines.collect do |line|
99
+ #puts "line: #{line}"
100
+ provider, country, sku, developer, title, version, product_type_id, units, developer_proceeds, begin_date, end_date, currency, country_code, currency_of_proceeds, apple_id, customer_price, promo_code, parent_id, subscription, period = line.split("\t")
101
+ p = {
102
+ :provider => provider.strip,
103
+ :country => country.strip,
104
+ :sku => sku.strip,
105
+ :developer => developer.strip,
106
+ :title => title.strip,
107
+ :version => version.strip,
108
+ :product_type_id => product_type_id.strip,
109
+ :units => units.to_i,
110
+ :developer_proceeds => developer_proceeds.to_f,
111
+ :begin_date => Date.strptime(begin_date.strip, '%m/%d/%Y'),
112
+ :end_date => Date.strptime(end_date.strip, '%m/%d/%Y'),
113
+ :currency => currency.strip,
114
+ :country_code => country_code.strip,
115
+ :currency_of_proceeds => currency_of_proceeds.strip,
116
+ :apple_id => apple_id.to_i,
117
+ :customer_price => customer_price.to_f,
118
+ :promo_code => promo_code.strip,
119
+ :parent_id => parent_id.strip,
120
+ :subscription => subscription.strip,
121
+ :period => period
122
+ }
123
+ puts "parsing failed".red if p==nil
124
+ p
125
+ end #lines collect
126
+ end #self.parse
127
+ end #class
128
+
129
+ puts "\nSales v#{VERSION}".red + " created by Your Headless Standup Programmer http://kitschmaster.com".dark_blue
130
+
131
+ if ARGV[0] == "-h" || ARGV[0] == "help" || ARGV[0] == "h"
132
+
133
+ puts USAGE
134
+
135
+ elsif ARGV[0] == "daily" || ARGV[0] == "weekly"
136
+ #compute alltime stats for daily or weekly reports
137
+
138
+ dir_filter = ARGV[0] == "daily" ? "D" : "W"
139
+
140
+ alltime_proceeds_per_currency = {} #currency is the key, value is the proceeds
141
+ alltime_renewables = 0
142
+ alltime_apps = {}
143
+ alltime_payed_units = 0
144
+ alltime_inapp_units = 0
145
+ alltime_free_units = 0
146
+ alltime_updated_units = 0
147
+ reports = Dir["S_#{dir_filter}_*.txt"].uniq.compact
148
+
149
+ if reports.empty?
150
+
151
+ puts "\nPlease download reports first.".red
152
+ puts "sale get:#{ARGV[0].split(':').last}\n".green
153
+
154
+ else
155
+
156
+ first_date = reports[0].split('_').last.split('.').first
157
+ reports.each do |alltime_filename|
158
+
159
+ #puts "Processing #{alltime_filename}".green
160
+
161
+ #get the date from the filename
162
+ date = alltime_filename.split('_').last.split('.').first #filename example: S_D_80076793_20120706.txt
163
+
164
+ report_data = File.open(alltime_filename, "rb").read
165
+
166
+ report = SalesReportParser.parse(report_data)
167
+ #puts report.class
168
+ if report #report parsed
169
+ apps = {}
170
+ total_payed_units = 0
171
+ total_inapp_units = 0
172
+ total_free_units = 0
173
+ total_updated_units = 0
174
+ report.each do |item| #report is a hash
175
+ if item
176
+ sku = item[:sku] #group data by app sku
177
+ if apps.has_key? sku #app is already cached
178
+ app = apps[sku]
179
+ else #initially insert app
180
+ app = {:sku=>sku, :title=>item[:title], :sold_units=>0, :updated_units=>0}
181
+ apps[sku] = app
182
+ end
183
+ #ensure currency sum
184
+ alltime_proceeds_per_currency[item[:currency_of_proceeds]] = 0.0 unless alltime_proceeds_per_currency[item[:currency_of_proceeds]]
185
+
186
+ #count units
187
+ if SALE_IDENTS.include? item[:product_type_id] #count sales
188
+ app[:sold_units] += item[:units]
189
+ if item[:customer_price]==0 #a free app
190
+ total_free_units += item[:units]
191
+ else
192
+ total_payed_units += item[:units]
193
+ alltime_proceeds_per_currency[item[:currency_of_proceeds]] += item[:developer_proceeds]
194
+ end
195
+ elsif INAPP_SALE_IDENTS.include? item[:product_type_id]
196
+ app[:sold_units] += item[:units]
197
+ total_inapp_units += item[:units]
198
+ alltime_proceeds_per_currency[item[:currency_of_proceeds]] += item[:developer_proceeds]
199
+ if item[:product_type_id] == "IAY" #InAppPurchase
200
+ alltime_renewables += item[:units]
201
+ end
202
+ elsif UPDATE_IDENTS.include? item[:product_type_id] #count updates
203
+ app[:updated_units] += item[:units]
204
+ total_updated_units += item[:units]
205
+ end
206
+ else # only if item
207
+ puts "null report".red
208
+ end
209
+ end
210
+
211
+ #add to the alltime stats
212
+ alltime_payed_units += total_payed_units
213
+ alltime_inapp_units += total_inapp_units
214
+ alltime_free_units += total_free_units
215
+ alltime_updated_units += total_updated_units
216
+
217
+ apps.each do |alltime_sku, apps_app|
218
+ #select the app
219
+ if alltime_apps.has_key? alltime_sku
220
+ #already cached
221
+ alltime_app = alltime_apps[alltime_sku]
222
+ else
223
+ #insert for the first time
224
+ alltime_app = {:sku=>alltime_sku, :title=>apps_app[:title], :sold_units=>0, :updated_units=>0}
225
+ alltime_apps[alltime_sku] = alltime_app
226
+ end
227
+ #add stats
228
+ alltime_app[:sold_units] += apps_app[:sold_units]
229
+ alltime_app[:updated_units] += apps_app[:updated_units]
230
+ end
231
+
232
+ =begin
233
+ #report for date
234
+ puts "\n\n______________________________________________________________".blue
235
+ puts "Report for #{date}"
236
+ puts "\n" + "Product".ljust(40).blue + ": " +"Downloads".green + " / " + "Updates".green
237
+ puts "______________________________________________________________".yellow
238
+ apps.each do |app_sku,apps_app|
239
+ puts "#{apps_app[:title].ljust(40).blue}: #{apps_app[:sold_units].to_s.ljust(10).green} / #{apps_app[:updated_units].to_s.rjust(7).dark_green}"
240
+ end
241
+ puts "______________________________________________________________".yellow
242
+ puts "#{'InApp Purchases'.ljust(40).green}: #{total_inapp_units}"
243
+ puts "#{'Payed Downloads'.ljust(40).green}: #{total_payed_units}"
244
+ puts "#{'Free Downloads'.ljust(40).dark_green}: #{total_free_units}"
245
+ puts "#{'Updates'.ljust(40).dark_green}: #{total_updated_units}"
246
+ puts "______________________________________________________________".blue
247
+ puts "\n\n"
248
+ =end
249
+
250
+ else
251
+ puts "null report parsed".red
252
+ end #if report parsed
253
+
254
+ end #reports.each
255
+
256
+ #report alltime
257
+ puts "\n\n______________________________________________________________".blue
258
+ from = Date.strptime first_date, '%Y%m%d'
259
+ age = Date.today - from
260
+ formatted_from = from.strftime("%b %d %Y")
261
+ puts "Report for #{ARGV[0]}, from #{formatted_from}, #{age.to_i} days"
262
+ puts "\n" + "Product".ljust(40).blue + ": " +"Downloads".green + " / " + "Updates".green
263
+ puts "______________________________________________________________".yellow
264
+ alltime_apps.each do |app_sku, aapp|
265
+ puts "#{aapp[:title].ljust(40).blue}: #{aapp[:sold_units].to_s.ljust(10).green} / #{aapp[:updated_units].to_s.rjust(7).dark_green}"
266
+ end
267
+ puts "______________________________________________________________".yellow
268
+ puts "#{'InApp Purchases'.ljust(40).green}: #{alltime_inapp_units}" + ( alltime_renewables > 0.0 ? " / #{alltime_renewables} Auto-Renewed" : "")
269
+ puts "#{'Payed Downloads'.ljust(40).green}: #{alltime_payed_units}"
270
+ puts "#{'Free Downloads'.ljust(40).dark_green}: #{alltime_free_units}"
271
+ puts "#{'Updates'.ljust(40).dark_green}: #{alltime_updated_units}"
272
+ puts "\n#{'Proceeds'.red}:\n\n"
273
+ total_eurs = 0.0
274
+ alltime_proceeds_per_currency.each do |proceed_key, proceed|
275
+ formatted_sum = proceed > 0.0 ? "#{proceed}".green : "#{proceed}".red
276
+ if proceed > 0.0
277
+ if proceed_key == "EUR"
278
+ total_eurs += proceed
279
+ puts "#{proceed_key} : #{formatted_sum}"
280
+ else
281
+ #convert using google
282
+ data = open("http://www.google.com/ig/calculator?q=#{proceed}#{proceed_key}=?EUR").read
283
+ #fix broken json
284
+ data.gsub!(/lhs:/, '"lhs":')
285
+ data.gsub!(/rhs:/, '"rhs":')
286
+ data.gsub!(/error:/, '"error":')
287
+ data.gsub!(/icc:/, '"icc":')
288
+ data.gsub!(Regexp.new("(\\\\x..|\\\\240)"), '')
289
+ #puts data
290
+ converted = JSON.parse data
291
+ converted_proceed = converted["rhs"].split(' ').first.to_f
292
+ total_eurs += converted_proceed
293
+ puts "#{proceed_key} : #{formatted_sum} / #{converted['rhs']}"
294
+ end
295
+ end
296
+ end
297
+ puts "\n#{'Total'.green}: #{total_eurs} Euros"
298
+ puts "______________________________________________________________".blue
299
+ puts "\n\n"
300
+
301
+ end #else reports.empty?
302
+
303
+ elsif ARGV[0] == "get:daily"
304
+
305
+ # Daily reports are available only for past 14 days, please enter a date within past 14 days.
306
+
307
+ first_date = Date.today
308
+
309
+ (1..14).each do |i|
310
+
311
+ date = (first_date - i).to_s.gsub('-', '')
312
+ puts "\nGetting Daily Sales Report for #{date}\n"
313
+
314
+ filename = "S_D_#{vendorId}_#{date}.txt"
315
+ unless File.exists? filename #download unless there already is a file
316
+ #call the java program and fetch the file
317
+ e = `java -cp #{classpath} Autoingestion #{username} #{password} #{vendorId} Sales Daily Summary #{date}`
318
+ report_file = e.split("\n").first
319
+ if File.exists? report_file
320
+ f = `gzip -df #{report_file}`
321
+ else
322
+ puts "#{e}\n".red
323
+ end
324
+ end
325
+
326
+ end # 91.each
327
+
328
+ elsif ARGV[0] == "get:weekly"
329
+
330
+ # Weekly reports are available only for past 13 weeks, please enter a weekend date within past 13 weeks.
331
+
332
+ first_date = Date.today
333
+ (1..13).each do |i| #13 weeks
334
+ date = (first_date - i*7)
335
+ day_increment = (0 - date.cwday) % 7
336
+ day_increment = 7 if day_increment == 0
337
+ next_sunday = date + day_increment
338
+ formatted_date = next_sunday.to_s.gsub('-', '')
339
+ puts "\nGetting Weekly Sales Report for #{formatted_date}\n"
340
+ filename = "S_W_#{vendorId}_#{formatted_date}.txt"
341
+ unless File.exists? filename #download unless there already is a file
342
+ #call the java program and fetch the file
343
+ e = `java -cp #{classpath} Autoingestion #{username} #{password} #{vendorId} Sales Weekly Summary #{formatted_date}`
344
+ report_file = e.split("\n").first
345
+ if File.exists? report_file
346
+ f = `gzip -df #{report_file}`
347
+ else
348
+ puts "#{e}\n".red
349
+ end
350
+ end
351
+ end # 13.each
352
+
353
+ else
354
+ # no argument or date in format YYYYMMDD
355
+ #get sales report for date
356
+
357
+ @date = ARGV[0]
358
+ date = (Date.today - 1).to_s.gsub('-', '')
359
+ date = @date if ARGV[0]
360
+ puts "\nDaily Sales Report for #{date}\n"
361
+
362
+ filename = "S_D_#{vendorId}_#{date}.txt"
363
+ unless File.exists? filename #download unless there already is a file
364
+ #call the java program and fetch the file
365
+ e = `java -cp #{classpath} Autoingestion #{username} #{password} #{vendorId} Sales Daily Summary #{date}`
366
+ report_file = e.split("\n").first
367
+ if File.exists? report_file
368
+ f = `gzip -df #{report_file}`
369
+ else
370
+ puts "#{e}\n".red
371
+ end
372
+ end
373
+
374
+ if File.exists? filename #only if there is data
375
+ #calculate totals
376
+ report_data = File.open(filename, "rb").read
377
+ report = SalesReportParser.parse(report_data)
378
+ apps = {}
379
+ total_payed_units = 0
380
+ total_inapp_units = 0
381
+ total_free_units = 0
382
+ total_updated_units = 0
383
+ report.each do |item|
384
+ sku = item[:sku] #group data by app sku
385
+ if apps.has_key? sku #app is already cached
386
+ app = apps[sku]
387
+ else #initially insert app
388
+ app = {:sku=>sku, :title=>item[:title], :sold_units=>0, :updated_units=>0}
389
+ apps[sku] = app
390
+ end
391
+ #count units
392
+ if SALE_IDENTS.include? item[:product_type_id] #count sales
393
+ app[:sold_units] += item[:units]
394
+ if item[:customer_price]==0 #a free app
395
+ total_free_units += item[:units]
396
+ else
397
+ total_payed_units += item[:units]
398
+ end
399
+ elsif INAPP_SALE_IDENTS.include? item[:product_type_id]
400
+ app[:sold_units] += item[:units]
401
+ total_inapp_units += item[:units]
402
+ elsif UPDATE_IDENTS.include? item[:product_type_id] #count updates
403
+ app[:updated_units] += item[:units]
404
+ total_updated_units += item[:units]
405
+ end
406
+ end
407
+
408
+ #report
409
+ puts "\n" + "Product".ljust(40).blue + ": " +"Downloads".green + " / " + "Updates".green
410
+ puts "____________________________________________________________".yellow
411
+ apps.each do |app_sku, app|
412
+ puts "#{app[:title].ljust(40).blue}: #{app[:sold_units].to_s.ljust(10).green} / #{app[:updated_units].to_s.rjust(7).dark_green}"
413
+ end
414
+ puts "____________________________________________________________".yellow
415
+ puts "#{'InApp Purchases'.ljust(40).green}: #{total_inapp_units}"
416
+ puts "#{'Payed Downloads'.ljust(40).green}: #{total_payed_units}"
417
+ puts "#{'Free Downloads'.ljust(40).dark_green}: #{total_free_units}"
418
+ puts "#{'Updates'.ljust(40).dark_green}: #{total_updated_units}"
419
+ puts "\n"
420
+ end
421
+ end
data/sales.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :username: YouriTunesUsername
3
+ :password: YouriTunesPassword
4
+ :vendorId: 80076733
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sales
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Mihael
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-07-07 00:00:00 Z
14
+ dependencies: []
15
+
16
+ description: iTunes Connect Command Line Autoingestion Script. Computes and presents totals. Uses Autoingestion.class for report downloading.
17
+ email: kitschmaster@gmail.com
18
+ executables:
19
+ - sale
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.md
24
+ files:
25
+ - Autoingestion.class
26
+ - README.md
27
+ - Rakefile
28
+ - VERSION.yml
29
+ - bin/sale
30
+ - sales.yml
31
+ homepage: http://github.com/mihael/sales
32
+ licenses: []
33
+
34
+ post_install_message:
35
+ rdoc_options: []
36
+
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ requirements: []
52
+
53
+ rubyforge_project: Sales
54
+ rubygems_version: 1.8.17
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: iTunes Connect Command Line Autoingestion Script.
58
+ test_files: []
59
+