Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
120
scripts/analyze_aso_keywords.mjs
Normal file
120
scripts/analyze_aso_keywords.mjs
Normal file
@@ -0,0 +1,120 @@
|
||||
// Script to query Apple iTunes Search API for ASO & Competition Analysis
|
||||
import https from 'https';
|
||||
|
||||
const KEYWORDS_DE = [
|
||||
'receipt scanner',
|
||||
'beleg scanner',
|
||||
'belege digitalisieren',
|
||||
'rechnung scanner',
|
||||
'kassenbon scanner',
|
||||
'quittung scanner',
|
||||
'spesen app',
|
||||
'buchhaltung scanner',
|
||||
'datev scanner',
|
||||
'receipt to excel',
|
||||
'expense tracker',
|
||||
'ausgaben tracker',
|
||||
'belegmanager',
|
||||
'rechnungsprogramm',
|
||||
'fahrtenbuch und belege',
|
||||
'steuer belege',
|
||||
'ocr scanner excel'
|
||||
];
|
||||
|
||||
const KEYWORDS_US = [
|
||||
'receipt scanner',
|
||||
'receipt to excel',
|
||||
'receipt scanner to excel',
|
||||
'expense tracker',
|
||||
'receipt keeper',
|
||||
'invoice scanner',
|
||||
'receipts and expenses',
|
||||
'ocr receipt scanner',
|
||||
'bookkeeping scanner',
|
||||
'mileage and receipts',
|
||||
'tax receipt organizer',
|
||||
'smart receipt'
|
||||
];
|
||||
|
||||
function fetchAppleSearch(term, country = 'de', limit = 25) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `https://itunes.apple.com/search?term=${encodeURIComponent(term)}&country=${country}&entity=software&limit=${limit}`;
|
||||
https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
resolve(json);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function analyzeKeywords(keywords, country) {
|
||||
console.log(`\n======================================================`);
|
||||
console.log(`🔍 ANALYSING APPLE APP STORE SEARCH DATA [Country: ${country.toUpperCase()}]`);
|
||||
console.log(`======================================================\n`);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const kw of keywords) {
|
||||
try {
|
||||
const data = await fetchAppleSearch(kw, country, 25);
|
||||
const totalResults = data.resultCount;
|
||||
const apps = data.results || [];
|
||||
|
||||
// Metrics calculation
|
||||
const top5 = apps.slice(0, 5);
|
||||
const top5Names = top5.map(a => a.trackName);
|
||||
const avgRating = top5.reduce((acc, a) => acc + (a.averageUserRating || 0), 0) / (top5.length || 1);
|
||||
const totalTop5Ratings = top5.reduce((acc, a) => acc + (a.userRatingCount || 0), 0);
|
||||
const avgTop5RatingCount = Math.round(totalTop5Ratings / (top5.length || 1));
|
||||
|
||||
// Check title keyword match density in top 10
|
||||
const titleMatches = apps.slice(0, 10).filter(a =>
|
||||
(a.trackName || '').toLowerCase().includes(kw.toLowerCase()) ||
|
||||
(a.description || '').toLowerCase().includes(kw.toLowerCase())
|
||||
).length;
|
||||
|
||||
// Price distribution
|
||||
const freeCount = top5.filter(a => a.price === 0).length;
|
||||
|
||||
results.push({
|
||||
keyword: kw,
|
||||
resultCount: totalResults,
|
||||
top1: apps[0] ? `${apps[0].trackName} (${apps[0].userRatingCount || 0} reviews, ★${apps[0].averageUserRating?.toFixed(1) || '0'})` : 'None',
|
||||
top5AvgRating: avgRating.toFixed(2),
|
||||
avgTop5Reviews: avgTop5RatingCount,
|
||||
titleMatchInTop10: titleMatches,
|
||||
topCompetitors: top5.map(a => ({
|
||||
name: a.trackName,
|
||||
seller: a.sellerName,
|
||||
reviews: a.userRatingCount || 0,
|
||||
rating: a.averageUserRating || 0,
|
||||
price: a.price,
|
||||
genres: a.genres
|
||||
}))
|
||||
});
|
||||
|
||||
// Avoid hitting rate limits
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
} catch (err) {
|
||||
console.error(`Error fetching "${kw}":`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const deResults = await analyzeKeywords(KEYWORDS_DE, 'de');
|
||||
const usResults = await analyzeKeywords(KEYWORDS_US, 'us');
|
||||
|
||||
console.log(JSON.stringify({ de: deResults, us: usResults }, null, 2));
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user