--json prints only JSON on stdout (withJsonOutput around every command: logs, tables and prompts to stderr; printJson everywhere; test runs every --json command against the fake stores and parses stdout); prices ppp compares like with like: par = the store's own US-equivalent price (Play convertRegionPrices, Apple equalizations; offline base x fx x (1 + VAT) rounded up) scaled by the PPP price level; defaults only lower, only price levels <= 0.5, World Bank high-income at par, CN at par on Apple and skipped on Play, never above par (--only-decreases, --max-ratio, --min-ratio, --max-level, --include-high-income; --floor / --ceiling kept), top-10 revenue market drops > 20 % warned (sales reports, else a static list), unchanged regions out of the sheet; price changes of existing products only with --allow-price-changes, TTY or not (a terminal yes covered them), then their own question listing each old -> new, held section in dry runs (play products, prices import, play / apple subscriptions); pitfalls 61-63; tests
This commit is contained in:
parent
bab3f63227
commit
357319a775
30 changed files with 865 additions and 209 deletions
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: ppp-pricing
|
||||
description: Propose purchasing power parity (PPP) prices for a mobile app's or game's in-app products in every App Store territory and Google Play region with `storebound prices ppp` - World Bank ICP PPP factors (bundled, CC BY 4.0), The Economist Big Mac index (downloaded on the owner's machine) or the owner's own factor CSV, clamped to a floor and ceiling of the US price, snapped to real App Store price points and clean local Play prices, written as a price sheet that `storebound prices import` applies after its own diff and confirmation. Use when the user asks for regional pricing, PPP or purchasing power pricing, cheaper prices in emerging markets, localized price tiers, or "price by country".
|
||||
description: Propose purchasing power parity (PPP) prices for a mobile app's or game's in-app products in every App Store territory and Google Play region with `storebound prices ppp` - World Bank ICP PPP factors (bundled, CC BY 4.0), The Economist Big Mac index (downloaded on the owner's machine) or the owner's own factor CSV, applied to the store's own US-equivalent price (VAT included) only in lower-income economies and never upward by default, snapped to real App Store price points and clean local Play prices, written as a price sheet that `storebound prices import` applies after its own diff and confirmation. Use when the user asks for regional pricing, PPP or purchasing power pricing, cheaper prices in emerging markets, localized price tiers, or "price by country".
|
||||
---
|
||||
|
||||
# PPP pricing (proposal sheet, both stores)
|
||||
|
|
@ -9,15 +9,31 @@ CLI: `storebound` (or `bunx storebound`, or `bun <toolchain>/cli/index.ts`). Thi
|
|||
CSV and prints a diff; nothing reaches a store until the owner runs `storebound prices import` and confirms there.
|
||||
|
||||
## What it computes
|
||||
Per product, store and region:
|
||||
- `usd = base x PPP factor / exchange rate` (the PPP price, back in US dollars at the market rate).
|
||||
- Clamped to `floor..ceiling` of the base (defaults 0.3 and 1.2): no region below 30 % or above 120 % of the US price.
|
||||
- Converted to the currency the store uses there (some App Store storefronts are priced in USD or EUR) and snapped:
|
||||
- App Store, online: the nearest real price point of that product in that territory (Apple takes nothing else).
|
||||
- Google Play, and the App Store offline: a clean local ending (x.99, 0.x9, whole numbers ending in 9; round
|
||||
numbers for JPY, KRW, VND, IDR, HUF, CLP, TWD and every value over 1000). Snapping can move a price a little past
|
||||
the floor or ceiling.
|
||||
- The US row is always the base itself.
|
||||
Like with like: store prices include VAT and follow today's exchange rate, the US price excludes sales tax, and the
|
||||
PPP price level compares net prices. So the proposal starts from the store's own conversion of the US price and
|
||||
scales it by the price level. Per product, store and region:
|
||||
- `level = PPP factor / exchange rate` (same year): the region's price level against the US.
|
||||
- `par` = the store's US-equivalent price there: Play `convertRegionPrices`, Apple the equalizations of the USA price
|
||||
point (both VAT included, today's rate). Offline: `base x rate x (1 + VAT)` rounded up to a clean price (standard
|
||||
VAT rates in `cli/lib/ppp.ts`; the dataset's rate stands in for today's).
|
||||
- `share`:
|
||||
- 1 (par) for the US, World Bank high-income economies (unless `--include-high-income`), mainland China, and every
|
||||
economy whose price level is above `--max-level` (default 0.5: only economies at or below half the US price
|
||||
level get a PPP price);
|
||||
- else `clamp(level, --min-ratio 0.3, --max-ratio 1)`: never below 30 % and never above the US-equivalent price.
|
||||
- `price = snap(par x share)`; with `--only-decreases` (the default; `--no-only-decreases` turns it off) never above
|
||||
the store's current price.
|
||||
- Snapping: App Store online, the nearest real price point of that product in that territory (Apple takes nothing
|
||||
else); Google Play and the App Store offline, a clean local ending (x.99, 0.x9, whole numbers ending in 9; round
|
||||
numbers for JPY, KRW, VND, IDR, HUF, CLP, TWD and every value over 1000).
|
||||
- China: Google Play is not available in mainland China (no Play rows); on the App Store CN keeps the store's price.
|
||||
- Top markets: a top-10 revenue market (from the sales reports in `.storebound/reports/`, `storebound reports
|
||||
apple-sales` / `play-sales`, else US JP CN KR GB DE FR CA AU TW) that would drop more than 20 % is warned about.
|
||||
- Only regions whose price changes go into the sheet (`rows`); regions that keep their price are listed as `kept`, so
|
||||
`prices import` leaves the store's own price there.
|
||||
|
||||
Example (bundled World Bank data, USD 4.99, defaults): Germany EUR 5.99 (high-income, par; the old method gave EUR
|
||||
3.59), UK, France, Japan at par; India and Turkey drop (price levels 0.23 and 0.41); Mexico stays (level above 0.5).
|
||||
|
||||
## Data sources (the owner picks one)
|
||||
- `worldbank` (default, bundled `cli/data/ppp-worldbank.json`): World Bank PA.NUS.PPP (PPP conversion factor, GDP)
|
||||
|
|
@ -28,17 +44,20 @@ Per product, store and region:
|
|||
(writes `.storebound/ppp-bigmac.json`, gitignored).
|
||||
- `file`: the owner's own CSV, `region,factor,currency,fx` (`fx` optional: then the bundled rate when the currency
|
||||
matches). `--file <csv>` or `prices.ppp.file` in the config.
|
||||
Config (optional): `prices: { ppp: { source, file, floor, ceiling, out } }`.
|
||||
Config (optional): `prices: { ppp: { source, file, minRatio, maxRatio, maxLevel, onlyDecreases, includeHighIncome, out } }`
|
||||
(`floor` / `ceiling` are the old names of `minRatio` / `maxRatio`).
|
||||
|
||||
## Steps
|
||||
1. `storebound check`. Products must be in `products[]` with a `usd` base. For real App Store price points the
|
||||
products must exist on App Store Connect (`storebound apple iap`); otherwise those rows are offline proposals.
|
||||
2. Dry run and show it to the owner:
|
||||
`storebound prices ppp --dry` (all products; `--only id1,id2`, `--store apple|play`, `--base 4.99` to try another
|
||||
US price, `--floor 0.4 --ceiling 1.1`, `--source bigmac|file`, `--offline` to skip every store read).
|
||||
US price, `--min-ratio 0.4`, `--max-level 0.6`, `--include-high-income`, `--source bigmac|file`, `--offline` to
|
||||
skip every store read). Change a default only when the owner asks for it.
|
||||
It prints the sheet, the diff against the stores' current prices (offline: every row "new") and the regions
|
||||
without a proposal.
|
||||
3. The owner decides: base price, floor and ceiling, and which regions to keep. Then write the sheet:
|
||||
3. The owner decides: base price, the rules above if not the defaults, and which regions to keep. Name every top-market
|
||||
warning to the owner. Then write the sheet:
|
||||
`storebound prices ppp` (default `store/prices-ppp.csv`, `--out` to change it).
|
||||
4. The owner reviews and edits the CSV (delete rows to keep a store's own conversion, round to a house price ladder).
|
||||
5. Apply through the normal price sheet flow: `storebound prices import --csv store/prices-ppp.csv --dry`, then
|
||||
|
|
@ -50,9 +69,8 @@ Config (optional): `prices: { ppp: { source, file, floor, ceiling, out } }`.
|
|||
- PPP is a proposal, not a rule. Cheaper prices can cannibalize, invite VPN arbitrage on web checkouts and change
|
||||
revenue; the owner decides region by region.
|
||||
- Taxes: App Store and Play customer prices are tax-inclusive in most regions (EU VAT, UK VAT, many others), while
|
||||
the US base is tax-exclusive. PPP factors describe consumer price levels including taxes, so the sheet is a
|
||||
customer price; proceeds per region differ by the local tax. Play's convertRegionPrices also returns tax-inclusive
|
||||
prices.
|
||||
the US base is tax-exclusive. That is why par is the store's own (tax-inclusive) conversion and the PPP level only
|
||||
scales it; never compare a PPP price in dollars with a local store price directly.
|
||||
- Offline values: Apple only accepts its own price points and its own storefront currency (USD or EUR in many
|
||||
smaller storefronts). An offline sheet is a starting point; `prices import --dry` shows what does not fit.
|
||||
- An App Store price increase cannot be undone once it starts.
|
||||
|
|
@ -60,9 +78,11 @@ Config (optional): `prices: { ppp: { source, file, floor, ceiling, out } }`.
|
|||
with `bun tools/update-ppp.ts` (`--check` prints its age).
|
||||
|
||||
## Output
|
||||
`--json` prints `{ source: {name, year, retrieved, url, license}, base: {product: usd}, floor, ceiling, online:
|
||||
{apple, play}, rows: [{product, store, region, currency, price, factor, fx, usdEquivalent, clamped, snappedFrom,
|
||||
snap}], skipped: [{product, store, region, why}], diff, out, written }`. Use `--json --dry` for a read-only run.
|
||||
`--json` prints only JSON on stdout (logs on stderr): `{ source: {name, year, retrieved, url, license}, base:
|
||||
{product: usd}, floor, ceiling, options, online: {apple, play}, rows: [{product, store, region, currency, price,
|
||||
factor, fx, level, par, parFrom, share, reason, current, usdEquivalent, snappedFrom, snap}], kept: [same shape],
|
||||
skipped: [{product, store, region, why}], diff, topMarkets: {markets, from, drops}, out, written }`. Use
|
||||
`--json --dry` for a read-only run.
|
||||
|
||||
## Out of scope
|
||||
- Writing prices to a store (that is `storebound prices import`, after the owner's yes).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue