@daemux/store-automator 0.10.41 → 0.10.43

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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "App Store & Google Play automation for Flutter apps",
8
- "version": "0.10.41"
8
+ "version": "0.10.43"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "store-automator",
13
13
  "source": "./plugins/store-automator",
14
14
  "description": "3 agents for app store publishing: reviewer, meta-creator, media-designer",
15
- "version": "0.10.41",
15
+ "version": "0.10.43",
16
16
  "keywords": [
17
17
  "flutter",
18
18
  "app-store",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemux/store-automator",
3
- "version": "0.10.41",
3
+ "version": "0.10.43",
4
4
  "description": "Full App Store & Google Play automation for Flutter apps with Claude Code agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "store-automator",
3
- "version": "0.10.41",
3
+ "version": "0.10.43",
4
4
  "description": "App Store & Google Play automation agents for Flutter app publishing",
5
5
  "author": {
6
6
  "name": "Daemux"
@@ -93,16 +93,19 @@ def create_subscription_availability(
93
93
 
94
94
  def get_subscription_prices(headers: dict, sub_id: str) -> list:
95
95
  """List existing prices for a subscription."""
96
- resp = requests.get(
97
- f"{BASE_URL}/subscriptions/{sub_id}/prices",
98
- params={"include": "subscriptionPricePoint"},
99
- headers=headers,
100
- timeout=TIMEOUT,
101
- )
102
- if not resp.ok:
103
- print_api_errors(resp, f"get prices for subscription {sub_id}")
104
- return []
105
- return resp.json().get("data", [])
96
+ url: str | None = f"{BASE_URL}/subscriptions/{sub_id}/prices"
97
+ params: dict | None = {"include": "subscriptionPricePoint", "limit": 200}
98
+ all_prices: list = []
99
+ while url:
100
+ resp = requests.get(url, headers=headers, params=params, timeout=TIMEOUT)
101
+ if not resp.ok:
102
+ print_api_errors(resp, f"get prices for subscription {sub_id}")
103
+ return all_prices
104
+ data = resp.json()
105
+ all_prices.extend(data.get("data", []))
106
+ url = data.get("links", {}).get("next")
107
+ params = None
108
+ return all_prices
106
109
 
107
110
 
108
111
  def get_price_points_for_territory(
@@ -153,13 +156,31 @@ def find_price_point_by_amount(
153
156
  return None
154
157
 
155
158
 
159
+ def get_price_point_equalizations(headers: dict, price_point_id: str) -> list:
160
+ """Get equalized price points for all territories from a base price point."""
161
+ url: str | None = f"{BASE_URL}/subscriptionPricePoints/{price_point_id}/equalizations"
162
+ params: dict | None = {"include": "territory", "limit": 200}
163
+ all_points: list = []
164
+ while url:
165
+ resp = requests.get(url, headers=headers, params=params, timeout=TIMEOUT)
166
+ if not resp.ok:
167
+ print_api_errors(resp, "get price point equalizations")
168
+ return all_points
169
+ data = resp.json()
170
+ all_points.extend(data.get("data", []))
171
+ url = data.get("links", {}).get("next")
172
+ params = None
173
+ return all_points
174
+
175
+
156
176
  def create_subscription_price(
157
177
  headers: dict,
158
178
  sub_id: str,
159
179
  price_point_id: str,
180
+ territory_id: str,
160
181
  start_date: str | None = None,
161
182
  ) -> dict | None:
162
- """Create a price entry for a subscription using a price point ID."""
183
+ """Create a price entry for a subscription using a price point ID and territory."""
163
184
  resp = requests.post(
164
185
  f"{BASE_URL}/subscriptionPrices",
165
186
  json={"data": {
@@ -170,6 +191,9 @@ def create_subscription_price(
170
191
  "subscriptionPricePoint": {
171
192
  "data": {"type": "subscriptionPricePoints", "id": price_point_id},
172
193
  },
194
+ "territory": {
195
+ "data": {"type": "territories", "id": territory_id},
196
+ },
173
197
  },
174
198
  }},
175
199
  headers=headers,
@@ -178,7 +202,6 @@ def create_subscription_price(
178
202
  if not resp.ok:
179
203
  print_api_errors(resp, f"create price for subscription {sub_id}")
180
204
  return None
181
- print(f" Set price for subscription {sub_id} (price point: {price_point_id})")
182
205
  return resp.json().get("data")
183
206
 
184
207
 
@@ -19,6 +19,7 @@ import hashlib
19
19
  import json
20
20
  import os
21
21
  import sys
22
+ import time
22
23
 
23
24
  from asc_iap_api import (
24
25
  create_group_localization,
@@ -38,6 +39,7 @@ from asc_subscription_setup import (
38
39
  create_subscription_availability,
39
40
  create_subscription_price,
40
41
  find_price_point_by_amount,
42
+ get_price_point_equalizations,
41
43
  get_price_points_for_territory,
42
44
  get_review_screenshot,
43
45
  get_subscription_availability,
@@ -173,40 +175,99 @@ def _sync_availability(headers: dict, sub_id: str, sub_config: dict) -> None:
173
175
 
174
176
 
175
177
  def _sync_pricing(headers: dict, sub_id: str, sub_config: dict) -> None:
176
- """Set subscription prices per territory from config."""
178
+ """Set subscription prices for all territories using Apple's equalization.
179
+
180
+ Finds the base price point (USD/USA), then uses the equalizations
181
+ endpoint to get Apple-calculated prices for all other territories.
182
+ """
177
183
  prices = sub_config.get("prices", {})
178
184
  if not prices:
179
185
  print(" WARNING: No prices configured, skipping pricing", file=sys.stderr)
180
186
  return
181
187
 
188
+ # Build set of territories that already have prices
182
189
  existing_prices = get_subscription_prices(headers, sub_id)
183
- if existing_prices:
184
- print(" Pricing already configured")
190
+ priced_territories: set[str] = set()
191
+ for ep in existing_prices:
192
+ pp_rel = ep.get("relationships", {}).get("subscriptionPricePoint", {})
193
+ pp_id = pp_rel.get("data", {}).get("id", "")
194
+ if "_" in pp_id:
195
+ priced_territories.add(pp_id.rsplit("_", 1)[-1])
196
+
197
+ # Use first configured currency as base (typically USD -> USA)
198
+ base_currency = next(iter(prices))
199
+ base_amount = prices[base_currency]
200
+ base_territory = CURRENCY_TO_TERRITORY.get(base_currency)
201
+ if not base_territory:
202
+ print(f" WARNING: Unknown base currency '{base_currency}'", file=sys.stderr)
203
+ return
204
+
205
+ # Find the base price point
206
+ price_points = get_price_points_for_territory(headers, sub_id, base_territory)
207
+ base_point = find_price_point_by_amount(price_points, base_amount)
208
+ if not base_point:
209
+ sample = [pp.get("attributes", {}).get("customerPrice", "?") for pp in price_points[:5]]
210
+ print(
211
+ f" WARNING: No price point matching {base_amount} for {base_territory}"
212
+ f" (API returned {len(price_points)} points, first prices: {sample})",
213
+ file=sys.stderr,
214
+ )
185
215
  return
186
216
 
187
- for currency, amount in prices.items():
188
- territory = CURRENCY_TO_TERRITORY.get(currency)
189
- if not territory:
190
- print(f" WARNING: Unknown currency '{currency}', skipping", file=sys.stderr)
217
+ created, skipped, failed = _apply_equalized_prices(
218
+ headers, sub_id, base_point, base_territory, base_amount, base_currency, priced_territories,
219
+ )
220
+ print(f" Pricing: {created} set, {skipped} existed, {failed} failed")
221
+
222
+
223
+ def _apply_equalized_prices(
224
+ headers: dict, sub_id: str, base_point: dict,
225
+ base_territory: str, base_amount: str, base_currency: str,
226
+ priced_territories: set[str],
227
+ ) -> tuple[int, int, int]:
228
+ """Set base price then apply Apple-equalized prices for all territories."""
229
+ created = 0
230
+ skipped = 0
231
+ failed = 0
232
+
233
+ # Set base territory price if not already set
234
+ if base_territory not in priced_territories:
235
+ result = create_subscription_price(headers, sub_id, base_point["id"], base_territory)
236
+ if result:
237
+ created += 1
238
+ print(f" Set base price {base_amount} {base_currency} for {base_territory}")
239
+ else:
240
+ failed += 1
241
+ print(f" WARNING: Failed to set base price for {base_territory}", file=sys.stderr)
242
+ return created, skipped, failed
243
+ else:
244
+ skipped += 1
245
+
246
+ # Get equalized prices for all other territories
247
+ equalized = get_price_point_equalizations(headers, base_point["id"])
248
+ if not equalized:
249
+ print(" WARNING: No equalizations returned", file=sys.stderr)
250
+ return created, skipped, failed
251
+
252
+ # Set price for each equalized territory
253
+ for eq_point in equalized:
254
+ territory_id = eq_point.get("relationships", {}).get(
255
+ "territory", {}
256
+ ).get("data", {}).get("id")
257
+ if not territory_id:
258
+ failed += 1
191
259
  continue
192
- price_points = get_price_points_for_territory(headers, sub_id, territory)
193
- point = find_price_point_by_amount(price_points, amount)
194
- if not point:
195
- sample = [
196
- pp.get("attributes", {}).get("customerPrice", "?")
197
- for pp in price_points[:5]
198
- ]
199
- print(
200
- f" WARNING: No price point matching {amount} for {territory}"
201
- f" (API returned {len(price_points)} points, first prices: {sample})",
202
- file=sys.stderr,
203
- )
260
+ if territory_id in priced_territories:
261
+ skipped += 1
204
262
  continue
205
- result = create_subscription_price(headers, sub_id, point["id"])
263
+ result = create_subscription_price(headers, sub_id, eq_point["id"], territory_id)
206
264
  if result:
207
- print(f" Set price {amount} for territory {territory}")
265
+ created += 1
208
266
  else:
209
- print(f" WARNING: Failed to set price for {territory}", file=sys.stderr)
267
+ failed += 1
268
+ time.sleep(0.05)
269
+
270
+ return created, skipped, failed
210
271
 
211
272
 
212
273
  def _sync_review_screenshot(