--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
|
|
@ -9,10 +9,11 @@
|
|||
// only, a future YYYY-MM-DD = scheduled change), kind (manual | auto; auto rows are Apple's equalized prices, exported
|
||||
// for reference and ignored on import).
|
||||
// import: reads the current prices, prints the diff table, and (not --dry) asks before writing. A product that already
|
||||
// has prices on a store is a price change: the owner's call. In a terminal (no --yes) the one question covers it;
|
||||
// --yes (also from `flow run --apply --yes`) only sets first prices and never changes existing ones, which need
|
||||
// --allow-price-changes: without it the run writes the first prices, prints "price change on <id> (<from> -> <to>)
|
||||
// needs the owner" per product and exits 3 (declined). The dry run marks those pairs "owner". Apple: one new price
|
||||
// has prices on a store is a price change: the owner's call, only with --allow-price-changes, TTY or not. The first
|
||||
// prices question (a terminal "y" or --yes, also from `flow run --apply --yes`) never covers them; with the flag they
|
||||
// get their own question listing each old -> new (lib/price-gate.ts). Without it the run writes the first prices,
|
||||
// prints "price change on <id> (<from> -> <to>) needs the owner" per product and exits 3 (declined). The dry run
|
||||
// shows them in its own "price changes on existing products (held)" section. Apple: one new price
|
||||
// schedule per changed product (base territory + every manual price; territories without one are equalized by Apple;
|
||||
// an increase cannot be undone once it starts). Play: the purchase option's regional prices are replaced: CSV regions
|
||||
// on top of the current ones; regions without a price get convertRegionPrices from the USD base (CSV US row, else the
|
||||
|
|
@ -22,7 +23,7 @@ import { ascClient, type Asc, type J } from './apple/asc';
|
|||
import type { Ctx } from './lib/ctx';
|
||||
import { confirm, need } from './lib/ctx';
|
||||
import { done, log, printJson, step, warn } from './lib/log';
|
||||
import { allowPriceChanges, type HeldPrice, holdPriceChanges } from './lib/price-gate';
|
||||
import { allowPriceChanges, type HeldPrice, heldSection, holdPriceChanges } from './lib/price-gate';
|
||||
import { toMoney } from './lib/money';
|
||||
import {
|
||||
appleSchedule, appleScheduleBody, applePriceRows, diffPrices, diffTable, parsePriceCsv, type PlayProduct, playPatchBody, playPriceRows, playRegions,
|
||||
|
|
@ -30,7 +31,6 @@ import {
|
|||
} from './lib/prices';
|
||||
import { playClient, type PlayClient } from './play/client';
|
||||
import { allSubscriptions, appleSubPriceRows, basePlanId, type PlaySubscription, playSubPriceRows } from './lib/subscriptions';
|
||||
import { stdinIsTTY } from './lib/tty';
|
||||
|
||||
const TAG = 'prices';
|
||||
|
||||
|
|
@ -153,22 +153,18 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const live = [...changed].filter(k => current.some(r => `${r.product}|${r.store}` === k && r.kind !== 'auto'));
|
||||
const first = [...changed].filter(k => !live.includes(k));
|
||||
if (live.length) log(TAG, `owner: price change of ${live.length} existing product / store pair(s) (--allow-price-changes): ${live.map(k => k.replace('|', ' ')).join(', ')}`);
|
||||
if (dry) { done(TAG, 'dry run: nothing written'); return; }
|
||||
let liveOk: boolean;
|
||||
if (stdinIsTTY() && !a.has('yes') && process.env.STOREBOUND_SERVE !== '1') {
|
||||
if (!(await confirm(ctx, `Write the new prices for ${changed.size} product / store pair(s)? Users see them as soon as the stores apply them; an App Store price increase cannot be undone once it starts.`))) return;
|
||||
liveOk = true;
|
||||
} else {
|
||||
if (first.length && !(await confirm(ctx, `Set the first prices of ${first.length} product / store pair(s) (${first.map(k => k.replace('|', ' ')).join(', ')})?`))) return;
|
||||
liveOk = await allowPriceChanges(ctx, live.length, `Change the prices of ${live.length} existing product / store pair(s) (${live.map(k => k.replace('|', ' ')).join(', ')})? Users see them as soon as the stores apply them; an App Store price increase cannot be undone once it starts.`);
|
||||
}
|
||||
const held: HeldPrice[] = [];
|
||||
if (!liveOk) for (const k of live) {
|
||||
changed.delete(k);
|
||||
const liveHeld: HeldPrice[] = live.map(k => {
|
||||
const rows = diff.filter(d => `${d.product}|${d.store}` === k && d.change !== 'same');
|
||||
held.push({ id: k.replace('|', ' '), changes: rows.length ? rows.map(d => ({ region: d.region, from: d.from === null ? '-' : `${d.currency} ${d.from}`, to: `${d.currency} ${d.to}${d.startDate ? ` from ${d.startDate}` : ''}` }))
|
||||
: [{ region: 'every region', from: 'current', to: 'reconverted' }] });
|
||||
}
|
||||
return { id: k.replace('|', ' '), changes: rows.length ? rows.map(d => ({ region: d.region, from: d.from === null ? '-' : `${d.currency} ${d.from}`, to: `${d.currency} ${d.to}${d.startDate ? ` from ${d.startDate}` : ''}` }))
|
||||
: [{ region: 'every region', from: 'current', to: 'reconverted' }] };
|
||||
});
|
||||
if (dry) { const h = heldSection(liveHeld, a.has('allow-price-changes')); if (h) console.log(h); done(TAG, 'dry run: nothing written'); return; }
|
||||
// first prices: one confirmation (terminal or --yes). Price changes of existing products: never part of it, only
|
||||
// with --allow-price-changes and their own question listing each old -> new (lib/price-gate.ts), TTY or not.
|
||||
if (first.length && !(await confirm(ctx, `Set the first prices of ${first.length} product / store pair(s) (${first.map(k => k.replace('|', ' ')).join(', ')})?`))) return;
|
||||
const liveOk = await allowPriceChanges(ctx, liveHeld, `Change the prices of ${live.length} existing product / store pair(s)? Users see them as soon as the stores apply them; an App Store price increase cannot be undone once it starts.`);
|
||||
const held: HeldPrice[] = [];
|
||||
if (!liveOk) { for (const k of live) changed.delete(k); held.push(...liveHeld); }
|
||||
|
||||
for (const p of products) {
|
||||
const wanted = wantedAll.filter(r => r.product === p.id);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue