Skip to main content
Glama
TrialAndErrorAI

App Store Connect MCP Server

finance-report-service.ts11.2 kB
/** * Service for handling Apple Finance Reports * These reports contain COMPLETE revenue including renewals */ import { AppStoreClient } from '../api/client.js'; import { gunzipSync } from 'zlib'; interface FinanceReportParams { reportType: 'FINANCIAL' | 'FINANCE_DETAIL'; regionCode: string; // 'US', 'Z1' (all regions), etc fiscalPeriod: string; // '2025-10' for July 2025 (Apple fiscal calendar) vendorNumber?: string; } interface ParsedFinanceReport { headers: string[]; rows: any[]; totalRevenue: number; rowCount: number; metadata: { reportType: string; regionCode: string; fiscalPeriod: string; }; } export class FinanceReportService { constructor( private client: AppStoreClient, private vendorNumber: string ) {} /** * Get financial report (aggregated monthly data) */ async getFinancialReport(params: { fiscalPeriod: string; // e.g., '2025-10' for July 2025 regionCode?: string; // Default to 'US' }): Promise<ParsedFinanceReport> { const reportParams = { 'filter[reportType]': 'FINANCIAL', 'filter[regionCode]': params.regionCode || 'US', 'filter[reportDate]': params.fiscalPeriod, 'filter[vendorNumber]': this.vendorNumber }; const response = await this.client.request('/financeReports', reportParams); return this.parseFinanceReport(response, { reportType: 'FINANCIAL', regionCode: params.regionCode || 'US', fiscalPeriod: params.fiscalPeriod }); } /** * Get detailed finance report (transaction-level data) */ async getFinanceDetailReport(params: { fiscalPeriod: string; // e.g., '2025-10' for July 2025 regionCode?: string; // Default to 'Z1' (all regions) }): Promise<ParsedFinanceReport> { const reportParams = { 'filter[reportType]': 'FINANCE_DETAIL', 'filter[regionCode]': params.regionCode || 'Z1', 'filter[reportDate]': params.fiscalPeriod, 'filter[vendorNumber]': this.vendorNumber }; const response = await this.client.request('/financeReports', reportParams); return this.parseFinanceReport(response, { reportType: 'FINANCE_DETAIL', regionCode: params.regionCode || 'Z1', fiscalPeriod: params.fiscalPeriod }); } /** * Parse finance report response */ private parseFinanceReport(data: any, metadata: any): ParsedFinanceReport { // Decompress if gzipped let content: string; if (Buffer.isBuffer(data)) { if (data.length > 2 && data[0] === 0x1f && data[1] === 0x8b) { content = gunzipSync(data).toString('utf-8'); } else { content = data.toString('utf-8'); } } else { content = typeof data === 'string' ? data : JSON.stringify(data); } // Parse TSV content const lines = content.split('\n').filter(l => l.trim()); if (lines.length === 0) { return { headers: [], rows: [], totalRevenue: 0, rowCount: 0, metadata }; } // Parse headers const headers = lines[0].split('\t').map(h => h.trim()); // Parse data rows const rows: any[] = []; let totalRevenue = 0; // Find key column indices const quantityIdx = headers.findIndex(h => h.toLowerCase().includes('quantity') || h === 'Units' ); const extendedShareIdx = headers.findIndex(h => h === 'Extended Partner Share' || h.includes('Extended') ); const partnerShareIdx = headers.findIndex(h => h === 'Partner Share' || h.includes('Partner Share') ); const salesReturnIdx = headers.findIndex(h => h === 'Sales or Return' || h.includes('Sales') ); const currencyIdx = headers.findIndex(h => h === 'Partner Share Currency' || h.includes('Currency') ); // Determine which column has the revenue const revenueIdx = extendedShareIdx >= 0 ? extendedShareIdx : partnerShareIdx; // Process each row for (let i = 1; i < lines.length; i++) { const values = lines[i].split('\t'); const row: any = {}; headers.forEach((header, idx) => { row[header] = values[idx] ? values[idx].trim() : ''; }); // Calculate revenue if (revenueIdx >= 0) { const revenueValue = parseFloat(values[revenueIdx] || '0'); // Check if it's a sale or return if (salesReturnIdx >= 0) { const salesOrReturn = values[salesReturnIdx]; if (salesOrReturn === 'S') { totalRevenue += revenueValue; } else if (salesOrReturn === 'R') { totalRevenue -= revenueValue; // Returns reduce revenue } } else { totalRevenue += revenueValue; } row._revenue = revenueValue; } rows.push(row); } return { headers, rows, totalRevenue, rowCount: rows.length, metadata }; } /** * Get monthly revenue summary - aggregates ALL regions */ async getMonthlySummary(year: number, month: number): Promise<{ totalRevenue: number; byProduct: Map<string, number>; byRegion: Map<string, number>; salesVsReturns: { sales: number; returns: number }; metadata: { fiscalPeriod: string; month: string; year: number; isLatestAvailable: boolean; }; }> { const fiscalPeriod = this.getFiscalPeriod(year, month); // FINANCIAL reports require aggregating specific regions // Z1 doesn't work, must fetch each region separately const regions = [ { code: 'US', name: 'United States' }, { code: 'CA', name: 'Canada' }, { code: 'EU', name: 'Europe' }, { code: 'JP', name: 'Japan' }, { code: 'AU', name: 'Australia' }, { code: 'WW', name: 'Rest of World' } ]; let totalRevenue = 0; const byProduct = new Map<string, number>(); const byRegion = new Map<string, number>(); let salesRevenue = 0; let returnsRevenue = 0; let hasData = false; // Fetch and aggregate all regions for (const { code, name } of regions) { try { const report = await this.getFinancialReport({ fiscalPeriod, regionCode: code }); if (report.totalRevenue > 0) { hasData = true; totalRevenue += report.totalRevenue; byRegion.set(name, report.totalRevenue); // Aggregate product data report.rows.forEach(row => { const productIdx = report.headers.findIndex(h => h === 'Vendor Identifier' || h === 'SKU' ); const salesReturnIdx = report.headers.findIndex(h => h === 'Sales or Return' ); if (productIdx >= 0) { const product = row[report.headers[productIdx]] || 'Unknown'; const revenue = row._revenue || 0; byProduct.set(product, (byProduct.get(product) || 0) + revenue); if (salesReturnIdx >= 0) { const salesOrReturn = row[report.headers[salesReturnIdx]]; if (salesOrReturn === 'S') { salesRevenue += revenue; } else if (salesOrReturn === 'R') { returnsRevenue += Math.abs(revenue); } } } }); } } catch (error: any) { // 404 is expected for regions without data if (!error.message.includes('404')) { console.error(`Error fetching ${name} data:`, error.message); } } } // Check if this is the latest available month const isLatestAvailable = await this.isLatestAvailableMonth(fiscalPeriod); return { totalRevenue, byProduct, byRegion, salesVsReturns: { sales: salesRevenue, returns: returnsRevenue }, metadata: { fiscalPeriod, month: `${year}-${String(month).padStart(2, '0')}`, year, isLatestAvailable } }; } /** * Get the latest available financial report */ async getLatestAvailable(): Promise<{ totalRevenue: number; byRegion: Map<string, number>; metadata: { fiscalPeriod: string; month: string; }; }> { // Try last 3 months to find latest available const now = new Date(); for (let i = 0; i < 3; i++) { const testDate = new Date(now.getFullYear(), now.getMonth() - i, 1); const year = testDate.getFullYear(); const month = testDate.getMonth() + 1; const fiscalPeriod = this.getFiscalPeriod(year, month); try { // Test with US region first (most likely to have data) await this.getFinancialReport({ fiscalPeriod, regionCode: 'US' }); // If successful, get full summary const summary = await this.getMonthlySummary(year, month); if (summary.totalRevenue > 0) { return { totalRevenue: summary.totalRevenue, byRegion: summary.byRegion, metadata: { fiscalPeriod, month: `${year}-${String(month).padStart(2, '0')}` } }; } } catch (error) { // Continue to next month } } throw new Error('No financial reports available in the last 3 months'); } /** * Check if a fiscal period is the latest available */ private async isLatestAvailableMonth(fiscalPeriod: string): Promise<boolean> { // Parse fiscal period (e.g., "2025-10" -> year 2025, fiscal month 10) const [fiscalYear, fiscalMonth] = fiscalPeriod.split('-').map(Number); // Try next fiscal period const nextFiscalMonth = fiscalMonth === 12 ? 1 : fiscalMonth + 1; const nextFiscalYear = fiscalMonth === 12 ? fiscalYear + 1 : fiscalYear; const nextPeriod = `${nextFiscalYear}-${String(nextFiscalMonth).padStart(2, '0')}`; try { // Check if next month is available await this.getFinancialReport({ fiscalPeriod: nextPeriod, regionCode: 'US' }); return false; // Next month exists, so this isn't latest } catch (error) { return true; // Next month doesn't exist, so this is latest } } /** * Convert regular calendar month/year to Apple fiscal period */ private getFiscalPeriod(year: number, month: number): string { // Apple fiscal year starts in October // Oct=1, Nov=2, Dec=3, Jan=4, Feb=5, Mar=6, Apr=7, May=8, Jun=9, Jul=10, Aug=11, Sep=12 const fiscalMonthMap: { [key: number]: number } = { 10: 1, // October 11: 2, // November 12: 3, // December 1: 4, // January 2: 5, // February 3: 6, // March 4: 7, // April 5: 8, // May 6: 9, // June 7: 10, // July 8: 11, // August 9: 12 // September }; const fiscalMonth = fiscalMonthMap[month]; const fiscalYear = month >= 10 ? year + 1 : year; return `${fiscalYear}-${String(fiscalMonth).padStart(2, '0')}`; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TrialAndErrorAI/appstore-connect-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server