From bbac32dd6ac2f0788e97da52ac0e64039500dae5 Mon Sep 17 00:00:00 2001 From: Gonzalo Eguiraun Date: Thu, 28 May 2026 07:56:03 +0200 Subject: [PATCH] fix(report-summary): multiply Developer Proceeds by Units in salesSummary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apple's SALES report `Developer Proceeds` column is the per-unit amount, not the row total. Each TSV row represents `Units` sales at `Developer Proceeds` each, so the row's contribution to total proceeds is Units × Developer Proceeds. The previous implementation summed the column value directly, ignoring Units, which undercounted any row with Units > 1. The undercount factor equals the average Units-per-row for the dataset — typically 5-10x on monthly reports for actively-selling apps. Same fix applied to subscriberSummary for symmetry, though SUBSCRIBER rows are per-transaction so Units is usually 1. Tests updated: - salesSummary_proceedsByCurrency: previously asserted the buggy values (sum of per-unit prices) — now asserts Units × proceeds. - Added salesSummary_proceedsScalesWithUnits as a regression test with a single Units=100 row. - Added salesSummary_perAppProceedsScalesWithUnits for the per-app accumulator path. --- Sources/asc-mcp/Helpers/ReportSummary.swift | 16 +++++--- .../HelperTests/ReportSummaryTests.swift | 38 +++++++++++++++++-- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Sources/asc-mcp/Helpers/ReportSummary.swift b/Sources/asc-mcp/Helpers/ReportSummary.swift index b2ff6f8..92a1acb 100644 --- a/Sources/asc-mcp/Helpers/ReportSummary.swift +++ b/Sources/asc-mcp/Helpers/ReportSummary.swift @@ -47,9 +47,12 @@ enum ReportSummary { totalUnits += units let currency = row["Currency of Proceeds"] ?? row["Customer Currency"] ?? "" - let proceeds = Double(row["Developer Proceeds"] ?? "") ?? 0.0 + // Apple's SALES report `Developer Proceeds` column is per-unit, not the row total. + // Multiply by Units to get the row's contribution to total proceeds. + let proceedsPerUnit = Double(row["Developer Proceeds"] ?? "") ?? 0.0 + let rowProceeds = proceedsPerUnit * Double(units) if !currency.isEmpty { - proceedsByCurrency[currency, default: 0.0] += proceeds + proceedsByCurrency[currency, default: 0.0] += rowProceeds } let country = row["Country Code"] ?? "" @@ -65,7 +68,7 @@ enum ReportSummary { let title = row["Title"] ?? "" if !title.isEmpty { appStats[title, default: AppSalesStats()].units += units - appStats[title, default: AppSalesStats()].proceedsByCurrency[currency, default: 0.0] += proceeds + appStats[title, default: AppSalesStats()].proceedsByCurrency[currency, default: 0.0] += rowProceeds } } @@ -247,9 +250,12 @@ enum ReportSummary { } let currency = row["Proceeds Currency"] ?? row["Customer Currency"] ?? "" - let proceeds = Double(row["Developer Proceeds"] ?? "") ?? 0.0 + // Apple's SUBSCRIBER report `Developer Proceeds` is per-unit; multiply by Units + // to get the row total (typically Units=1 per row, but be defensive). + let proceedsPerUnit = Double(row["Developer Proceeds"] ?? "") ?? 0.0 + let rowProceeds = proceedsPerUnit * Double(units) if !currency.isEmpty { - proceedsByCurrency[currency, default: 0.0] += proceeds + proceedsByCurrency[currency, default: 0.0] += rowProceeds } let country = row["Country"] ?? "" diff --git a/Tests/ASCMCPTests/HelperTests/ReportSummaryTests.swift b/Tests/ASCMCPTests/HelperTests/ReportSummaryTests.swift index 6d9b882..c8b7317 100644 --- a/Tests/ASCMCPTests/HelperTests/ReportSummaryTests.swift +++ b/Tests/ASCMCPTests/HelperTests/ReportSummaryTests.swift @@ -25,17 +25,48 @@ struct ReportSummaryTests { } @Test func salesSummary_proceedsByCurrency() { + // Apple's `Developer Proceeds` column is per-unit. Each row represents `Units` + // sales at `Developer Proceeds` each, so the row's contribution to the total is + // Units × Developer Proceeds. let rows: [[String: String]] = [ - ["Units": "10", "Developer Proceeds": "69.90", "Currency of Proceeds": "USD"], - ["Units": "5", "Developer Proceeds": "34.95", "Currency of Proceeds": "EUR"], - ["Units": "3", "Developer Proceeds": "20.10", "Currency of Proceeds": "USD"] + ["Units": "10", "Developer Proceeds": "6.99", "Currency of Proceeds": "USD"], + ["Units": "5", "Developer Proceeds": "6.99", "Currency of Proceeds": "EUR"], + ["Units": "3", "Developer Proceeds": "6.70", "Currency of Proceeds": "USD"] ] let summary = ReportSummary.salesSummary(from: rows) let proceeds = summary["proceeds_by_currency"] as? [String: Double] + // USD: 10 × 6.99 + 3 × 6.70 = 69.90 + 20.10 = 90.00 #expect(proceeds?["USD"] == 90.0) + // EUR: 5 × 6.99 = 34.95 #expect(proceeds?["EUR"] == 34.95) } + @Test func salesSummary_proceedsScalesWithUnits() { + // Regression test: previously `Developer Proceeds` was summed directly, + // ignoring `Units`, which undercounted multi-unit rows. + let rows: [[String: String]] = [ + // 100 sales at $0.70 each in one aggregated row. + ["Units": "100", "Developer Proceeds": "0.70", "Currency of Proceeds": "USD"] + ] + let summary = ReportSummary.salesSummary(from: rows) + let proceeds = summary["proceeds_by_currency"] as? [String: Double] + #expect(proceeds?["USD"] == 70.0) // 100 × 0.70, not 0.70 + } + + @Test func salesSummary_perAppProceedsScalesWithUnits() { + // Same regression for the per-app breakdown (by_app). + let rows: [[String: String]] = [ + ["Title": "MyApp", "Units": "10", "Developer Proceeds": "0.70", "Currency of Proceeds": "USD"], + ["Title": "MyApp", "Units": "5", "Developer Proceeds": "0.62", "Currency of Proceeds": "EUR"] + ] + let summary = ReportSummary.salesSummary(from: rows) + let byApp = summary["by_app"] as? [[String: Any]] + #expect(byApp?.count == 1) + let stats = byApp?.first?["proceeds_by_currency"] as? [String: Double] + #expect(stats?["USD"] == 7.0) // 10 × 0.70 + #expect(stats?["EUR"] == 3.10) // 5 × 0.62 + } + @Test func salesSummary_topCountries() { let rows: [[String: String]] = [ ["Units": "50", "Country Code": "US"], @@ -65,6 +96,7 @@ struct ReportSummaryTests { } @Test func salesSummary_proceedsRounding() { + // Each row has Units=1 so row proceeds == per-unit proceeds. let rows: [[String: String]] = [ ["Units": "1", "Developer Proceeds": "1.111", "Currency of Proceeds": "USD"], ["Units": "1", "Developer Proceeds": "2.222", "Currency of Proceeds": "USD"]