sales 0.0.1

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.
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
+