iTunes-Connect - Sales-Reports

Wer Apple und iTunes-Connect (ITC) kennt, weiß von den Problemchen welche ITC teilweise mit sich bringt. Mich persönlich hat immer gestört, das die Sales-Reports / Verkaufszahlen des Vortages manchmal erst am späten Abend des Folgetages verfügbar waren. Das liegt wohl daran, das die Apple-Server die Reports für jeden Entwickler aufbereiten müssen und es hier öfter mal zu Engpässen an Rechenressourcen kommt. Das ist natürlich nur eine Vermutung meinerseits...Wie dem auch sei! ;-)

itc_umsatz_rechnerIch habe mir vor ca. einem Jahr ein kleines Tool in Java namens "ITC - Umsatz-Rechner" (ja ja, ich weiß, ziemlich einfallsreich ;) ) geschrieben, welches die Roh-Daten der Verkäufe für einen beliebigen Zeitraum mit Hilfe der "Autoingestion.class" (Download über Entwickler-Account bei Apple) herunter lädt und diese Daten auswertet. Heraus kommt dann eine HTML-Datei, welche alle Verkaufszahlen aller Apps des gewählten Zeitraumes enthält. Häufig vorkommende Währungen wie USD, GBP, AUD, CAD und z.B. CHF werden anhand eines hinterlegten Wechselkurses direkt in Euro (EUR) umgerechnet. Des weiteren werden Zusammenfassung für die einzelnen Länder erstellt, woraus hervor geht, in welchem Land wieviele Neuinstallationen, Updates oder InApp-Käufe getätigt wurden. Das Ganze dann nochmal separiert nach iPhone, iPad oder Mac.

ITC - Umsatz-Rechner - Funktionsweise

christ schmuck.jpgZunächst holen wir uns eine Kalendar-Instanz damit wir Apples Autoingestion.class mitteilen können, welchen Zeitraum oder welches Datum wir gerne hätten.

Calendar cal = Calendar.getInstance();
String sDateType, sDateFormat;

if (!bLoadTagesansicht){
//monatlich
sDateType = "Monthly";
sDateFormat = "yyyyMM";
cal.add(Calendar.MONTH, -1);
}else{
//täglich
sDateType = "Daily";
sDateFormat = "yyyyMMdd";
cal.add(Calendar.DATE, iHeuteMinusXTage);
/*
//jährlich
sDateType = "Yearly";
sDateFormat = "yyyy";
cal.add(Calendar.YEAR, -1);
*/
}

String sDate = new SimpleDateFormat(sDateFormat).format(cal.getTime());
String sDateTypeShort = sDateType.substring(0, 1);

Jetzt bilden wir die entsprechende Befehlszeile für die Autoingestion.class und rufen diese mittels exec() auf und warten 2-3 Sekunden bis der Download abgeschlossen ist.

String sCmd = "java Autoingestion"
+ " " + sUser
+ " " + sPass
+ " " + sVendorID
+ " Sales "
+ sDateType
+ " Summary"
+ " " + sDate;

Process proc = Runtime.getRuntime().exec(sCmd);

InputStream inputStream = proc.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

proc.waitFor();

String line;
while ((line = bufferedReader.readLine()) != null)
{
//echo(line);
}

Nachdem die Roh-Daten aus dem ITC-Account herunter geladen wurden, kann anhand des gewählten Zeitraumes und der Vendor-ID der Dateiname der herunter geladenen Datei gebildet werden. Diese ist mit GZip komprimiert und wird daher im nächsten Step entpackt, und im Anschluß geparsed bzw. verarbeitet. Hinweis: Die verwendeten Hilfsklassen wie Downloader.java und myAppUmsatzEntry.java werden weiter unten näher erläutert. Ist an dieser Stelle erstmal nicht ganz so wichtig.

String sFile = "S_" + sDateTypeShort + "_" + sVendorID + "_" + sDate + ".txt.gz";
//echo("Suche " + sFile + "...");

String sDataFile = "data.txt";
if (downloader.unzip(sFile, sDataFile)){
calculate(sDataFile, sSaveAs);
}else{
//echo("Daten fuer " + sDate + " liegen noch nicht vor. :/");
}

Die Funktion calculate() ließt die Datei mit den Rohdaten aus ITC ein, öffnet ein neues HTML-Dokument und wandelt die Daten kurz gesagt in etwas schöneres, aber vor allem leserlicheres um. Zunächst wird eine ArrayList erstellt, in welche die einzelnen Daten geschrieben werden.

ArrayList<myAppUmsatzEntry> umsaetze = new ArrayList<myAppUmsatzEntry>();

Dann wird die Datei mit den Rohdaten eingelesen, die entsprechenden Spalten und Daten-Abschnitte zugeordnet und im Array zwischengespeichert.

FileReader fr = new FileReader(sFile);
BufferedReader br = new BufferedReader(fr);

String[] arr;
String s = br.readLine(); //erste zeile überspringen//head
while ((s = br.readLine())!=null)
{
arr = s.split("\t");
if (arr.length>=20)
{
myAppUmsatzEntry umsatzEntry = new myAppUmsatzEntry();

umsatzEntry.sSKU = arr[2];
umsatzEntry.sVersion = arr[5];
umsatzEntry.sType = arr[6];
umsatzEntry.iMenge = Integer.parseInt(arr[7]);
umsatzEntry.fGewinn = Float.parseFloat(arr[8]);
umsatzEntry.sWaehrung = arr[11];
umsatzEntry.sLand = arr[12];

if (sDatumVon.equalsIgnoreCase(""))
{
sDatumVon = arr[9];
sDatumBis = arr[10];

if (sDatumVon.equalsIgnoreCase(sDatumBis))
bIsTagesansicht = true;

downloader.echo("Statistik: " + sDatumVon + " bis " + sDatumBis + "\n");
}

//zwischenspeichern
umsaetze.add(umsatzEntry);
}
}
br.close();

Jetzt wo alle Daten formatiert im Arbeitsspeicher bzw. der ArrayList liegen, sortieren wir diese noch nach der SKU, damit alle Einträge mit der jeweiligen App-Kennung zusammen liegen.

Collections.sort(umsaetze, new SortBySku());

Nun wird eine neue HTML-Datei geöffnet, und als erstes eine Überschrift definiert, welches das aktuelle Datum bzw. den gewählten Zeitraum der Daten beinhaltet.

String sOutputFile = sSaveAs; //sDatumVon + "_bis_" + sDatumBis + ".html";
sOutputFile = sOutputFile.replace("/", "_");

PrintWriter pWriter = new PrintWriter(new FileWriter(sOutputFile), true);
pWriter.println(getHtmlStart() + "<h1>" + sDatumVon + " bis " + sDatumBis + "</h1>" + nl + nl);

Nun werden die einzelnen Einträge der ArrayList abgearbeitet, und der entsprechende Output erzeugt. Hier werden die Wechselkurse mit den Verkäufen in den entsprechenden Ländern verrechnet und formatiert ausgegeben.

.....................

pWriter.println(nl + "</div><b>" + sLastSKU + " - Installationen: " + String.valueOf(iInstallLastSku) + " - Updates: " + String.valueOf(iUpdateLastSku) + "</b>" + nl
+ "Gewinn:"
+ " " + downloader.getPreisFormated(fEuroLastSku) + " EUR"
+ " + " + downloader.getPreisFormated(fUsdLastSku) + " USD"
+ " + " + downloader.getPreisFormated(fGbpLastSku) + " GBP"
+ " + " + downloader.getPreisFormated(fAudLastSku) + " AUD"
+ " + " + downloader.getPreisFormated(fCadLastSku) + " CAD"
+ " + " + downloader.getPreisFormated(fChfLastSku) + " CHF"
+ " + X (andere W&auml;hrungen)"
+ nl);

float fTmp = fEuroLastSku
+ (fUsdLastSku * getWechselkurs("usd"))
+ (fGbpLastSku * getWechselkurs("gbp"))
+ (fAudLastSku * getWechselkurs("aud"))
+ (fCadLastSku * getWechselkurs("cad"))
+ (fChfLastSku * getWechselkurs("chf"));
pWriter.println("<h3>Gewinn EUR - kalkuliert: " + downloader.getPreisFormated(fTmp) + " EUR</h3>" + nl);

.....................

An dieser Stelle spare ich mir einige Code-Abschnitte, einfach um diese Seite hier nicht noch weiter zu flooten :D Den gesamten Code als Download gibt es weiter unten auf der Seite.

Helferklasse - Downloader.java

amz box.jpgDie Klasse Downloader enthält diverse Hilfsfunktionen. Unter anderem auch Download- und Rundungsfunktionen. Zum entpacken des GZip-Archives nutze ich eine Funktion namens unzip(String sFile, String sSaveAS).

Der erste Parameter ist der Pfad zum GZip-Archiv, und der zweite Parameter beinhaltet den Pfad bzw. Dateinamen, unter welchem die entpackte Datei gespeichert werden soll.

public static boolean unzip(String sFile, String sSaveAS)
{
try
{
File f = new File(sFile);
if (!f.exists())
return false;

GZIPInputStream gzipInputStream = null;
gzipInputStream = new GZIPInputStream(new FileInputStream(sFile));
OutputStream out = new FileOutputStream(sSaveAS);

byte[] buf = new byte[1024];
int len;
while ((len = gzipInputStream.read(buf)) > 0) {
out.write(buf, 0, len);
}
gzipInputStream.close();
out.close();
f.delete();

return true;
}
catch (Exception ex)
{
echo("error unzip: " + ex.getLocalizedMessage());
}
return false;
}

Helferklasse - myAppUmsatzEntry.java

Die Klasse myAppUmsatzEntry ist eine Struktur die Informationen zur App, Version, dem entsprechenden Land, Anzahl Verkäufe bzw. Downloads, dem Gewinn und einigem mehr enthält. Neben dem Speichern von Informationen gibt es auch noch eine Funktion namens getTypeAsString() welche anhand des Type-Codes des Eintrages einen lesbaren String zurück gibt. So muss man nicht schauen das 7T ein Update auf iPad war. Hier die Funktion auf einen Blick, sollte selbsterklärend sein. ;-)

public String getTypeAsString()
{
if (sType.equalsIgnoreCase("1"))
return "Free/paid";
else if (sType.equalsIgnoreCase("7"))
return "Update";
else if (sType.equalsIgnoreCase("IA1") || sType.equalsIgnoreCase("IA9") || sType.equalsIgnoreCase("IAY") || sType.equalsIgnoreCase("IAC"))
return "InApp";
else if (sType.equalsIgnoreCase("1F"))
return "Free/paid";
else if (sType.equalsIgnoreCase("7F"))
return "Update";
else if (sType.equalsIgnoreCase("1T"))
return "Free/paid (iPad)";
else if (sType.equalsIgnoreCase("7T"))
return "Update (iPad)";
else if (sType.equalsIgnoreCase("F1"))
return "Free/paid (Mac)";
else if (sType.equalsIgnoreCase("F7"))
return "Update (Mac)";
else if (sType.equalsIgnoreCase("FI1"))
return "InApp (Mac)";
else if (sType.equalsIgnoreCase("1E"))
return "Paid (iPhone/iPod)";
else if (sType.equalsIgnoreCase("1EP"))
return "Paid (iPad)";
else if (sType.equalsIgnoreCase("1EU"))
return "Paid (Universal)";
else
return "Unknown";
}

Schlußwort und Download-Links zum Code

Die Idee hinter diesem Tool ist simpel, die Umsetzung relativ gut gelungen. Da das Tool nebenbei und auf die schnelle geschrieben wurde, könnte hier und da der Code sicher noch etwas optimiert werden, aber alles in allem Funktioniert es wie es soll.

Hier gibt es die entsprechenden Klassen und den sonst noch benötigten Code zum Download.
Viel Spaß damit! :-)

Bei Fragen, Anmerkungen oder Erweiterungsvorschlägen nutze bitte das Kontaktformular.

Author: Sascha von Saschas-Bastelstube.de

Autor: sascha

Ein paar Worte über mich selbst. Mein Name ist Sascha, wie der Titel dieses Blogs erahnen lässt. :-) Ich bin von Beruf selbstständiger Programmierer und Trader. Neben Programmierung, Trading und Kampfsport sind einige meiner Interessen und Hobbys das Kochen, Fitness und das Hören lauter Heavy-Metal Musik. :D

Dies könnte dich auch interessieren

DynDNS-Alternative im Eigenbau

Wer ohnehin über einen eigenen Webserver/Webspace im Internet und etwas Programmierkenntnisse verfügt, kann sich ohne großen Aufwand einen Service wie DynDNS.org & Co. sparen, will man auf Teile seines Heimnetzwerkes von Unterwegs aus zugreifen. Solche Services stellen einen Hostnamen/URL bereit (z.B. mein-heimnetzwerk.dyndns.org) welche dann auf die IP-Adresse des Heimnetzwerkes weitergeleitet wird. Neben einem oft kostenpflichtigem … „DynDNS-Alternative im Eigenbau“ weiterlesen


Gitarrenhalter für die Wand aus dem 3D-Drucker

Gitarrenhalter für die Wand oder auch Gitarrenständer sind eine schöne Sache, insbesondere wenn sich die Sammlung an Musikinstrumenten über die Jahre vergrößert. Mir persönlich gefallen Wandhalterungen ganz gut, da Sie nicht nur elegant Stauraum schaffen, sondern gleichzeitig auch ein echter Hingucker sind. Dem Raum verleihen Akustikgitarre, E-Gitarre, Bass, Banjo oder Ukulele einen gewissen Charakter wenn … „Gitarrenhalter für die Wand aus dem 3D-Drucker“ weiterlesen


Color-Calculator / Farbrechner

Der Color-Calculator errechnet aus Hex-Farbwerten die entsprechenden Dezimal- und Float-Werte. So kann man Beispielsweise schnell und einfach Farbewerte aus dem Web für seine iOS-App übernehmen, ohne manuell umrechnen zu müssen. Für alle Tippfaulen unter euch (zu denen ich mich im übrigen auch zähle ;-) ) wird der passende Objective-C Programmcode generiert. Die Eingabe von ganzen … „Color-Calculator / Farbrechner“ weiterlesen


Telegram Messenger und die Bot API

Telegram ist im Prinzip ein Messenger ähnlich Whatsapp oder Threema , kostenlos und leicht zu bedienen. Ich möchte nicht sagen das dieser besser oder schlechter ist als andere Messenger, allerdings gefällt mir die Telegram Bot API sogut, dass ich an dieser Stelle ein kleines Tutorial dazu schreiben möchte. Inhaltsverzeichnis:Telegram Messenger Bot API programmierenSchritt 1 - … „Telegram Messenger und die Bot API“ weiterlesen


WordPress-Installation anpassen für Datenschutz-Grundverordnung (DSGVO) 2018

Die EU-Datenschutz-Grundverordnung (DSGVO) tritt am 25. Mai 2018 in Kraft. Dabei gibt es unter anderem für Webseitenbetreiber allerhand zu beachten. Detailierte Informationen und Text-Muster für die eigene Datenschutzerklärung gibt es seit langem auf diversen Internetseiten wie z.B. bei Datenschutz.org , aber auch in entsprechenden Büchern zur Datenschutz Grundverordnung 2018 . Inhaltsverzeichnis:WordPress für Datenschutz-Grundverordnung (DSGVO) anpassenGoogle … „WordPress-Installation anpassen für Datenschutz-Grundverordnung (DSGVO) 2018“ weiterlesen