appstore-api-mcp 1.12.0 → 1.15.0

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.
@@ -0,0 +1,176 @@
1
+ country_code,country_name,gdp_per_capita,category,default_coefficient
2
+ LUX,Luxembourg,146818,premium,1.10
3
+ IRL,Ireland,129132,premium,1.10
4
+ BMU,Bermuda,125842,premium,1.10
5
+ CHE,Switzerland,111047,premium,1.10
6
+ ISL,Iceland,98150,premium,1.10
7
+ CYM,Cayman Islands,97583,premium,1.10
8
+ SGP,Singapore,94481,premium,1.10
9
+ NOR,Norway,91884,premium,1.10
10
+ USA,United States,89599,usa,1.00
11
+ DNK,Denmark,76581,high_income,0.90
12
+ MAC,Macau,74921,high_income,0.90
13
+ NLD,Netherlands,73174,high_income,0.90
14
+ QAT,Qatar,71441,high_income,0.90
15
+ AUS,Australia,65946,high_income,0.90
16
+ SWE,Sweden,62036,high_income,0.90
17
+ AUT,Austria,61694,high_income,0.90
18
+ BEL,Belgium,60418,high_income,0.90
19
+ ISR,Israel,60009,high_income,0.90
20
+ DEU,Germany,59925,high_income,0.90
21
+ HKG,Hong Kong,56844,high_income,0.90
22
+ GBR,United Kingdom,56661,high_income,0.90
23
+ FIN,Finland,56084,high_income,0.90
24
+ CAN,Canada,54935,high_income,0.90
25
+ ARE,United Arab Emirates,51348,high_income,0.90
26
+ NZL,New Zealand,49383,upper_middle,0.75
27
+ MLT,Malta,49277,upper_middle,0.75
28
+ FRA,France,48982,upper_middle,0.75
29
+ ITA,Italy,43161,upper_middle,0.75
30
+ CYP,Cyprus,42413,upper_middle,0.75
31
+ BHS,Bahamas,39726,upper_middle,0.75
32
+ ESP,Spain,38040,upper_middle,0.75
33
+ TWN,Taiwan,37827,upper_middle,0.75
34
+ SVN,Slovenia,37178,upper_middle,0.75
35
+ KOR,South Korea,35962,upper_middle,0.75
36
+ SAU,Saudi Arabia,35231,upper_middle,0.75
37
+ CZE,Czechia,35161,upper_middle,0.75
38
+ JPN,Japan,34713,upper_middle,0.75
39
+ VGB,British Virgin Islands,34200,upper_middle,0.75
40
+ EST,Estonia,34041,upper_middle,0.75
41
+ MSR,Montserrat,34000,upper_middle,0.75
42
+ BRN,Brunei,33858,upper_middle,0.75
43
+ LTU,Lithuania,32982,upper_middle,0.75
44
+ PRT,Portugal,31415,upper_middle,0.75
45
+ GUY,Guyana,31378,upper_middle,0.75
46
+ KWT,Kuwait,30805,upper_middle,0.75
47
+ TCA,Turks and Caicos Islands,30349,upper_middle,0.75
48
+ BHR,Bahrain,29253,upper_middle,0.75
49
+ AIA,Anguilla,28850,upper_middle,0.75
50
+ SVK,Slovakia,28524,upper_middle,0.75
51
+ POL,Poland,28485,upper_middle,0.75
52
+ GRC,Greece,27170,upper_middle,0.75
53
+ HRV,Croatia,26958,upper_middle,0.75
54
+ BRB,Barbados,25927,upper_middle,0.75
55
+ HUN,Hungary,25916,upper_middle,0.75
56
+ LVA,Latvia,25630,upper_middle,0.75
57
+ URY,Uruguay,24380,lower_middle,0.50
58
+ ROU,Romania,22436,lower_middle,0.50
59
+ ATG,Antigua and Barbuda,22314,lower_middle,0.50
60
+ KNA,Saint Kitts and Nevis,22158,lower_middle,0.50
61
+ SYC,Seychelles,21956,lower_middle,0.50
62
+ BGR,Bulgaria,20426,lower_middle,0.50
63
+ PAN,Panama,19802,lower_middle,0.50
64
+ PLW,Palau,19456,lower_middle,0.50
65
+ OMN,Oman,19119,lower_middle,0.50
66
+ CRI,Costa Rica,19104,lower_middle,0.50
67
+ MDV,Maldives,18684,lower_middle,0.50
68
+ TUR,Turkey,18198,lower_middle,0.50
69
+ TTO,Trinidad and Tobago,18121,lower_middle,0.50
70
+ RUS,Russia,17446,lower_middle,0.50
71
+ CHL,Chile,17181,lower_middle,0.50
72
+ SRB,Serbia,15322,lower_middle,0.50
73
+ MNE,Montenegro,14986,lower_middle,0.50
74
+ KAZ,Kazakhstan,14723,lower_middle,0.50
75
+ LCA,Saint Lucia,14647,lower_middle,0.50
76
+ ARG,Argentina,14359,lower_middle,0.50
77
+ NRU,Nauru,14274,lower_middle,0.50
78
+ MEX,Mexico,13967,lower_middle,0.50
79
+ MYS,Malaysia,13901,lower_middle,0.50
80
+ CHN,China,13806,lower_middle,0.50
81
+ GRD,Grenada,12544,lower_middle,0.50
82
+ MUS,Mauritius,12519,lower_middle,0.50
83
+ DOM,Dominican Republic,11919,lower_middle,0.50
84
+ VCT,Saint Vincent and the Grenadines,11132,lower_middle,0.50
85
+ ALB,Albania,11108,lower_middle,0.50
86
+ TKM,Turkmenistan,10801,lower_middle,0.50
87
+ BRA,Brazil,10578,lower_middle,0.50
88
+ MKD,North Macedonia,10378,lower_middle,0.50
89
+ GEO,Georgia,10126,lower_middle,0.50
90
+ DMA,Dominica,9944,emerging,0.40
91
+ BIH,Bosnia and Herzegovina,9648,emerging,0.40
92
+ BLR,Belarus,9435,emerging,0.40
93
+ GAB,Gabon,9303,emerging,0.40
94
+ PER,Peru,9256,emerging,0.40
95
+ ARM,Armenia,8969,emerging,0.40
96
+ JAM,Jamaica,8405,emerging,0.40
97
+ COL,Colombia,8249,emerging,0.40
98
+ MDA,Moldova,8239,emerging,0.40
99
+ XKS,Kosovo,8033,emerging,0.40
100
+ THA,Thailand,7942,emerging,0.40
101
+ BLZ,Belize,7897,emerging,0.40
102
+ AZE,Azerbaijan,7365,emerging,0.40
103
+ ECU,Ecuador,7210,emerging,0.40
104
+ MNG,Mongolia,7005,emerging,0.40
105
+ BWA,Botswana,6943,emerging,0.40
106
+ LBY,Libya,6866,emerging,0.40
107
+ SUR,Suriname,6843,emerging,0.40
108
+ FJI,Fiji,6825,emerging,0.40
109
+ PRY,Paraguay,6799,emerging,0.40
110
+ ZAF,South Africa,6667,emerging,0.40
111
+ GTM,Guatemala,6478,emerging,0.40
112
+ UKR,Ukraine,6382,emerging,0.40
113
+ DZA,Algeria,6095,emerging,0.40
114
+ TON,Tonga,5921,emerging,0.40
115
+ IRQ,Iraq,5832,emerging,0.40
116
+ SLV,El Salvador,5744,emerging,0.40
117
+ CPV,Cape Verde,5671,emerging,0.40
118
+ FSM,Micronesia,5241,emerging,0.40
119
+ IDN,Indonesia,5074,emerging,0.40
120
+ JOR,Jordan,4908,emerging,0.40
121
+ NAM,Namibia,4816,emerging,0.40
122
+ MAR,Morocco,4763,emerging,0.40
123
+ LBN,Lebanon,4757,emerging,0.40
124
+ TUN,Tunisia,4752,emerging,0.40
125
+ VNM,Vietnam,4745,emerging,0.40
126
+ BOL,Bolivia,4585,emerging,0.40
127
+ SWZ,Eswatini,4410,emerging,0.40
128
+ LKA,Sri Lanka,4325,emerging,0.40
129
+ PHL,Philippines,4321,emerging,0.40
130
+ BTN,Bhutan,4285,emerging,0.40
131
+ STP,Sao Tome and Principe,4061,emerging,0.40
132
+ UZB,Uzbekistan,3647,emerging,0.40
133
+ HND,Honduras,3637,emerging,0.40
134
+ GHA,Ghana,3193,emerging,0.40
135
+ EGY,Egypt,3191,emerging,0.40
136
+ VUT,Vanuatu,3133,emerging,0.40
137
+ VEN,Venezuela,3103,emerging,0.40
138
+ ZWE,Zimbabwe,3071,emerging,0.40
139
+ CIV,Cote d'Ivoire,3016,emerging,0.40
140
+ NIC,Nicaragua,2953,emerging,0.40
141
+ AGO,Angola,2931,emerging,0.40
142
+ IND,India,2818,emerging,0.40
143
+ KHM,Cambodia,2812,emerging,0.40
144
+ KGZ,Kyrgyzstan,2790,emerging,0.40
145
+ MRT,Mauritania,2582,emerging,0.40
146
+ PNG,Papua New Guinea,2555,emerging,0.40
147
+ KEN,Kenya,2549,emerging,0.40
148
+ COG,Republic of the Congo,2420,emerging,0.40
149
+ SLB,Solomon Islands,2386,emerging,0.40
150
+ LAO,Laos,2174,emerging,0.40
151
+ CMR,Cameroon,2027,emerging,0.40
152
+ SEN,Senegal,1921,emerging,0.40
153
+ PAK,Pakistan,1707,emerging,0.40
154
+ TJK,Tajikistan,1644,emerging,0.40
155
+ BEN,Benin,1635,emerging,0.40
156
+ NPL,Nepal,1535,emerging,0.40
157
+ ZMB,Zambia,1353,emerging,0.40
158
+ UGA,Uganda,1353,emerging,0.40
159
+ TZA,Tanzania,1302,emerging,0.40
160
+ GNB,Guinea-Bissau,1225,emerging,0.40
161
+ NGA,Nigeria,1200,emerging,0.40
162
+ TCD,Chad,1139,emerging,0.40
163
+ BFA,Burkina Faso,1115,emerging,0.40
164
+ MMR,Myanmar,1097,emerging,0.40
165
+ RWA,Rwanda,1043,emerging,0.40
166
+ MLI,Mali,1014,emerging,0.40
167
+ SLE,Sierra Leone,980,emerging,0.40
168
+ LBR,Liberia,904,emerging,0.40
169
+ GMB,Gambia,890,emerging,0.40
170
+ NER,Niger,789,emerging,0.40
171
+ COD,Democratic Republic of the Congo,772,emerging,0.40
172
+ MOZ,Mozambique,690,emerging,0.40
173
+ MWI,Malawi,622,emerging,0.40
174
+ MDG,Madagascar,616,emerging,0.40
175
+ YEM,Yemen,415,emerging,0.40
176
+ AFG,Afghanistan,380,emerging,0.40
package/src/ppp.js ADDED
@@ -0,0 +1,183 @@
1
+ // Purchasing-power-parity (PPP) pricing: country data + pricing math + grid
2
+ // snapping. Pure logic only (no network) so it is unit-testable; the App Store
3
+ // Connect calls live in index.js.
4
+ //
5
+ // Logic and the countries.csv dataset are ported from the MIT-licensed
6
+ // appstore-ppp-prices by Aleksandr Belousov
7
+ // (https://github.com/duceum/appstore-ppp-pricing-agent-skill).
8
+
9
+ import { readFileSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import { dirname, join } from "node:path";
12
+
13
+ const HERE = dirname(fileURLToPath(import.meta.url));
14
+
15
+ // Per-tier price floors: premium/usa/high_income can't go below 0.99; the
16
+ // lower tiers can drop to 0.49.
17
+ export const MINIMUM_PRICE_HIGH = 0.99;
18
+ export const MINIMUM_PRICE_LOW = 0.49;
19
+ const HIGH_MINIMUM_CATEGORIES = new Set(["premium", "usa", "high_income"]);
20
+
21
+ // Categories an agent may override a coefficient for (USA is always the 1.0
22
+ // base and is excluded from the target set).
23
+ export const OVERRIDABLE_CATEGORIES = new Set([
24
+ "premium",
25
+ "high_income",
26
+ "upper_middle",
27
+ "lower_middle",
28
+ "emerging",
29
+ ]);
30
+
31
+ /** Floor for a category's price. */
32
+ export function minimumPrice(category) {
33
+ return HIGH_MINIMUM_CATEGORIES.has(category)
34
+ ? MINIMUM_PRICE_HIGH
35
+ : MINIMUM_PRICE_LOW;
36
+ }
37
+
38
+ /**
39
+ * Load the embedded country table. Skips USA and any excluded 3-letter codes.
40
+ * Returns [{ code, name, gdpPerCapita, category, defaultCoefficient, minimumPrice }].
41
+ */
42
+ export function loadCountries(exclude, csvPath = join(HERE, "ppp-countries.csv")) {
43
+ const excluded = new Set((exclude || []).map((c) => c.trim().toUpperCase()));
44
+ const text = readFileSync(csvPath, "utf8");
45
+ const lines = text.split(/\r?\n/).filter((l) => l.trim().length);
46
+ const header = lines[0].split(",");
47
+ const idx = (name) => header.indexOf(name);
48
+ const iCode = idx("country_code");
49
+ const iName = idx("country_name");
50
+ const iGdp = idx("gdp_per_capita");
51
+ const iCat = idx("category");
52
+ const iCoeff = idx("default_coefficient");
53
+
54
+ const countries = [];
55
+ for (const line of lines.slice(1)) {
56
+ const cells = line.split(",");
57
+ const code = (cells[iCode] || "").trim().toUpperCase();
58
+ if (!code || code === "USA" || excluded.has(code)) continue;
59
+ const gdp = parseInt(cells[iGdp], 10);
60
+ const coeff = parseFloat(cells[iCoeff]);
61
+ if (!Number.isFinite(gdp) || !Number.isFinite(coeff)) continue; // skip bad rows
62
+ const category = (cells[iCat] || "").trim();
63
+ countries.push({
64
+ code,
65
+ name: (cells[iName] || "").trim(),
66
+ gdpPerCapita: gdp,
67
+ category,
68
+ defaultCoefficient: coeff,
69
+ minimumPrice: minimumPrice(category),
70
+ });
71
+ }
72
+ return countries;
73
+ }
74
+
75
+ /** Resolve the coefficient for a country: an override for its tier wins. */
76
+ export function getCoefficient(country, overrides) {
77
+ if (overrides && Object.prototype.hasOwnProperty.call(overrides, country.category))
78
+ return overrides[country.category];
79
+ return country.defaultCoefficient;
80
+ }
81
+
82
+ /**
83
+ * Validate an agent-supplied {category: coefficient} override map.
84
+ * Returns the normalized map or throws a friendly Error.
85
+ */
86
+ export function validateCoefficients(overrides) {
87
+ const out = {};
88
+ for (const [rawCat, rawVal] of Object.entries(overrides || {})) {
89
+ const cat = String(rawCat).trim().toLowerCase();
90
+ if (!OVERRIDABLE_CATEGORIES.has(cat))
91
+ throw new Error(
92
+ `Unknown coefficient category '${rawCat}'. Valid: ${[...OVERRIDABLE_CATEGORIES].join(", ")}.`,
93
+ );
94
+ const val = Number(rawVal);
95
+ if (!Number.isFinite(val) || val < 0.1 || val > 2.0)
96
+ throw new Error(`Coefficient for '${cat}' must be in [0.1, 2.0], got ${rawVal}.`);
97
+ out[cat] = Math.round(val * 1000) / 1000;
98
+ }
99
+ return out;
100
+ }
101
+
102
+ /**
103
+ * Target USD prices for all products across all countries.
104
+ * products: [{ id, name, productId, usPrice }]
105
+ * Ratio preservation: if the cheapest product × coefficient would fall below a
106
+ * country's floor, every product in that country is scaled up by the same
107
+ * factor so price ratios between products are kept intact.
108
+ */
109
+ export function calculateTargetPrices(products, countries, overrides) {
110
+ if (!products?.length || !countries?.length) return [];
111
+ const cheapestUs = Math.min(...products.map((p) => p.usPrice));
112
+ const results = [];
113
+ for (const country of countries) {
114
+ const baseCoeff = getCoefficient(country, overrides);
115
+ const coeff = Math.max(baseCoeff, country.minimumPrice / cheapestUs);
116
+ for (const p of products) {
117
+ results.push({
118
+ countryCode: country.code,
119
+ countryName: country.name,
120
+ category: country.category,
121
+ productId: p.id,
122
+ productName: p.name || p.productId,
123
+ usPrice: p.usPrice,
124
+ coefficient: coeff,
125
+ targetPriceUsd: Math.round(p.usPrice * coeff * 100) / 100,
126
+ });
127
+ }
128
+ }
129
+ return results;
130
+ }
131
+
132
+ /**
133
+ * Pick the price point closest to `target`, rounding away from `base`.
134
+ * All three are in the same currency. An exact match wins; above-base targets
135
+ * round up and below-base targets round down, so a market priced above base
136
+ * never slips under its target and a discounted one never creeps over it.
137
+ * Falls back to the nearest point when the chosen direction holds none.
138
+ * points: [{ id, customerPrice, territory3 }]
139
+ */
140
+ export function findPricePoint(points, target, base) {
141
+ if (!points?.length) return null;
142
+ const exact = points.find((p) => Math.abs(p.customerPrice - target) < 0.005);
143
+ if (exact) return exact;
144
+ if (target > base) {
145
+ const higher = points.filter((p) => p.customerPrice > target);
146
+ if (higher.length)
147
+ return higher.reduce((a, b) => (b.customerPrice < a.customerPrice ? b : a));
148
+ } else if (target < base) {
149
+ const lower = points.filter((p) => p.customerPrice < target);
150
+ if (lower.length)
151
+ return lower.reduce((a, b) => (b.customerPrice > a.customerPrice ? b : a));
152
+ }
153
+ return points.reduce((a, b) =>
154
+ Math.abs(b.customerPrice - target) < Math.abs(a.customerPrice - target) ? b : a,
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Decode the 3-letter territory from a base64-encoded price-point id.
160
+ * Apple encodes a JSON blob like {"t":"CHE",...} in the id.
161
+ */
162
+ export function decodeTerritory(pricePointId) {
163
+ try {
164
+ const decoded = Buffer.from(pricePointId + "==", "base64").toString("utf8");
165
+ return JSON.parse(decoded).t || "";
166
+ } catch {
167
+ return "";
168
+ }
169
+ }
170
+
171
+ /** The default coefficient + floor for each tier, for tool docs / previews. */
172
+ export function tierSummary() {
173
+ const seen = new Map();
174
+ for (const c of loadCountries()) {
175
+ if (!seen.has(c.category))
176
+ seen.set(c.category, {
177
+ category: c.category,
178
+ defaultCoefficient: c.defaultCoefficient,
179
+ minimumPrice: c.minimumPrice,
180
+ });
181
+ }
182
+ return [...seen.values()];
183
+ }
@@ -0,0 +1,45 @@
1
+ // Subscription-offer helpers. Pure logic (no network) so it is unit-testable.
2
+ //
3
+ // Overlapping introductory/promotional offers on the same subscription make
4
+ // StoreKit drop the product (seen as a sandbox `countMismatch`). This finds
5
+ // offers whose active date ranges intersect so a tool can flag them.
6
+
7
+ /** Parse a YYYY-MM-DD date to a comparable number; null/undefined = open-ended. */
8
+ function toTime(d, openEndedValue) {
9
+ if (d == null || d === "") return openEndedValue;
10
+ const t = Date.parse(d);
11
+ return Number.isNaN(t) ? openEndedValue : t;
12
+ }
13
+
14
+ /**
15
+ * Given offers [{ id, startDate, endDate, kind? }], return the pairs whose
16
+ * [startDate, endDate] ranges overlap. A missing startDate is treated as "from
17
+ * the beginning of time" and a missing endDate as "forever" (open-ended), which
18
+ * is how an always-on introductory offer behaves — so two open-ended offers
19
+ * always overlap.
20
+ */
21
+ export function detectOverlappingOffers(offers) {
22
+ const ranges = (offers || []).map((o) => ({
23
+ id: o.id,
24
+ kind: o.kind,
25
+ startDate: o.startDate ?? null,
26
+ endDate: o.endDate ?? null,
27
+ start: toTime(o.startDate, -Infinity),
28
+ end: toTime(o.endDate, Infinity),
29
+ }));
30
+ const overlaps = [];
31
+ for (let i = 0; i < ranges.length; i++) {
32
+ for (let j = i + 1; j < ranges.length; j++) {
33
+ const a = ranges[i];
34
+ const b = ranges[j];
35
+ // Half-open overlap: they share at least one instant.
36
+ if (a.start <= b.end && b.start <= a.end) {
37
+ overlaps.push({
38
+ a: { id: a.id, kind: a.kind, startDate: a.startDate, endDate: a.endDate },
39
+ b: { id: b.id, kind: b.kind, startDate: b.startDate, endDate: b.endDate },
40
+ });
41
+ }
42
+ }
43
+ }
44
+ return overlaps;
45
+ }