Skip to content

feat: add the territory-pricing skill - #3

Merged
ns-vasilev merged 9 commits into
mainfrom
feat/territory-pricing
Oct 30, 2026
Merged

ns-vasilev merged 9 commits into
mainfrom
feat/territory-pricing

Conversation

@ns-vasilev

Copy link
Copy Markdown
Member

Add the app-store-suite plugin with a skill that sets worldwide per-territory prices for an app, in-app purchase or subscription group in App Store Connect.

  • add pricing strategies: GNI bands, PPP, Big Mac index, tiered and Apple equalization, with a fallback chain and clamp guard rails
  • add check_setup.sh to verify the API key, its permissions and a live round trip (distinguishes 401 from 403)
  • build a Price Proposal first; apply.rb is a dry run unless --confirm, runs subscriptions as canary then rest, reads prices back after every write and supports rollback
  • bundle World Bank and Economist data snapshots as an offline fallback
  • add minitest suite and a test workflow; exclude the data CSVs from codespell

Add the app-store-suite plugin with a skill that sets worldwide
per-territory prices for an app, in-app purchase or subscription
group in App Store Connect.

- add pricing strategies: GNI bands, PPP, Big Mac index, tiered and
  Apple equalization, with a fallback chain and clamp guard rails
- add check_setup.sh to verify the API key, its permissions and a
  live round trip (distinguishes 401 from 403)
- build a Price Proposal first; apply.rb is a dry run unless
  --confirm, runs subscriptions as canary then rest, reads prices
  back after every write and supports rollback
- bundle World Bank and Economist data snapshots as an offline
  fallback
- add minitest suite and a test workflow; exclude the data CSVs
  from codespell
@ns-vasilev ns-vasilev self-assigned this Oct 30, 2026
The Asc tests built EC keys with generate_key and set the public
key by hand. Keys are immutable since OpenSSL 3.0, so both failed
on the CI Ruby.

- generate keys with EC.generate, which works on old and new
  OpenSSL
- verify the JWT signature with the key itself instead of a
  hand-built public key
A price in effect can sit on a legacy price point that Apple no
longer lists. Its equalizations request returns 404, and a
territory on such a point looked changed even when the price
stayed the same.

- map the current point onto the listed point with the same price
- resolve the base point before reading equalizations
- add regression tests
Move the skill toward the vibe-aso model: the agent drives the App
Store Connect API from written instructions, and code stays only
where it must be exact.

- merge the API client into one asc.rb and refuse POST, PATCH and
  DELETE without --write, so no write happens before consent
- add plan.rb for the arithmetic only: strategies, price point
  snapping, clamp, legacy price point resolution and CSV edits
- turn SKILL.md and asc-pricing-api.md into an agent playbook with
  the lessons of the first live run
- read the bundled data snapshots in plan.rb; refresh_data.rb
  fetches new data on demand
- drop estimate, apply, snapshot, list_products, the product
  adapters and their tests
- ignore .DS_Store and the root territory-pricing run folder
A subscription has about 800 price points per territory, so reading
all territories takes about 700 requests. One sequential script ran
for over 8 minutes, wrote its result only at the end and would be
killed by the 10 minute shell limit.

- add collect_points.rb: one batch of territories per run, read in
  parallel threads, one file per territory, resumes where it stopped
- report failed territories and exit non-zero instead of hiding them
- tell the agent in SKILL.md and the playbook to sweep in short
  commands and never in one long loop
- add tests for batching, resume and failure handling
The nearest Apple price point often looks random: a territory has
about 800 of them. Add an endings option that picks the nearest
point with an ending customary in the currency.

- add plan.rb --endings charm|none, --endings-tolerance (default
  5%) and --endings-file
- add references/data/endings.json with the conventions per
  currency: .99/.49 by default, .90 for BRL and ILS, .98 for SGD,
  round numbers for JPY, KRW, INR, MXN and CNY
- let the window reach past the clamp bounds, otherwise band 3
  territories sitting on the clamp floor could only round up
- ask for the endings next to the strategy in SKILL.md
- add tests, including the shipped rules file
A review of the territory-pricing branch found ten defects, three
of which could change live prices.

- keep the current price point on skipped rows so an app or IAP
  schedule never drops a manual price
- merge partial tier ratios with the defaults and reject a tier
  without a ratio instead of pricing it at 100%
- never clamp the base territory's ratio
- rebuild with the options of the original run
- read comma-decimal and currency-symbol prices in proposal.csv,
  name unreadable cells and write nothing until they are fixed
- reset ratio, target and clamped flag on edited rows
- add & to a path that already has a query and report any worker
  error in collect_points.rb
- use github.ref in the CI concurrency groups
- drop the unused split_entries
- fetch the World Bank country list once and share the snapshot
  paths in one module
- add a test for each fix
Subscriptions and a lifetime unlock are priced in separate runs, so a
re-price could leave yearly above lifetime, or change the lifetime
price a day before the subscriptions, without anyone noticing.

- add ladder.rb: per territory a longer subscription must cost more
  in total and less per month, and a lifetime unlock at least twice
  the cheapest yearly cost (--lifetime-min-years)
- block consent in SKILL.md while the ladder has violations
- add schedule_body.rb: build the app or IAP price schedule from a
  proposal, with --start-date so changed territories switch on the
  same day as the subscriptions
- add product.period to plan-input.json for the ladder check
- describe the family flow in the playbook
- add tests, including the command line tools

The dated app and IAP schedule follows Apple's examples and is not
verified against the live API.
A second review found ten defects. Four could produce wrong prices:
an untouched skipped row was re-snapped on rebuild, 1.099 was read
as 1099, the ladder missed a second plan of the same period, and a
target near zero could pick a free price point.

- keep untouched CSV cells, skipped rows included, on --rebuild
- read an ambiguous three-digit value by the row's price
- compare every pair of subscription periods in the ladder
- never choose a 0.00 price point; skip a list without a paid one
- lock the token and credentials in asc.rb for parallel collection
- report an accepted write with a non-JSON body as a success and
  check the request body before sending anything
- refresh each data source on its own in refresh_data.rb
- share the tie-break rule, the transient option list and the base
  territory handling
- add a test for each fix
@ns-vasilev
ns-vasilev merged commit 1d1a989 into main Oct 30, 2026
2 checks passed
@ns-vasilev
ns-vasilev deleted the feat/territory-pricing branch October 30, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant