// Price changes of existing (live) products: the owner's call, never confirmed by --yes. Used by play products, // play subscriptions, apple subscriptions and prices import (apple iap never changes the prices of an existing product). // Imported only by those commands, so --allow-price-changes is a flag of theirs alone (tools/gen-flags.ts). import { confirm, type Ctx, declined } from './ctx'; import { stdinIsTTY } from './tty'; /** The flag that lets a write command change the price of an existing product. `flow run` never adds it and refuses it in steps. */ export const ALLOW_PRICE_CHANGES = 'allow-price-changes'; /** A held price change of one existing product: per region / territory, current -> wanted. */ export interface HeldPrice { id: string; changes: { region: string; from: string; to: string }[] } /** * Price changes of existing (live) products are the owner's call, and only ever with --allow-price-changes, TTY or * not: no confirmation of creates or other updates (a terminal "y", `--yes`, `flow run --apply --yes`) covers them. * Without the flag: false (the command applies everything else and ends with holdPriceChanges()). With the flag they * get their own question that lists each change old -> new: asked in a terminal (even with --yes, which never answers * it there), answered by --yes outside a terminal (the flag plus --yes is the explicit owner's run), by the desktop * owner dialog under serve. Nothing to change: true. */ export async function allowPriceChanges(ctx: Ctx, changes: HeldPrice[], question: string): Promise { if (!changes.length) return true; if (!ctx.args.has(ALLOW_PRICE_CHANGES)) return false; const list = changes.flatMap(h => h.changes.map(c => ` ${h.id} ${c.region} ${c.from} -> ${c.to}`)).join('\n'); const q = `${question}\n${list}\n`; return confirm(stdinIsTTY() && process.env.STOREBOUND_SERVE !== '1' ? withoutYes(ctx) : ctx, q); } /** The end of a run that left price changes alone: one line per product, then declined (exit DECLINED_EXIT), so a flow stops there. */ export function holdPriceChanges(held: HeldPrice[]): void { if (!held.length) return; for (const h of held) console.error(`price change on ${h.id} (${priceDiff(h.changes)}) needs the owner: re-run with --${ALLOW_PRICE_CHANGES}`); declined(`${held.length} price change(s) of existing products need the owner (re-run with --${ALLOW_PRICE_CHANGES}); everything else was applied`); } /** "US USD 2.99 -> USD 3.99, DE ..." (at most 4 regions, then "+N more"). Pure. */ export function priceDiff(changes: HeldPrice['changes']): string { const shown = changes.slice(0, 4).map(c => `${c.region} ${c.from} -> ${c.to}`).join(', '); return changes.length > 4 ? `${shown}, +${changes.length - 4} more` : shown; } /** The same context with --yes hidden: for questions --yes must not answer. */ export function withoutYes(ctx: Ctx): Ctx { return { ...ctx, args: { ...ctx.args, has: (n: string) => n !== 'yes' && ctx.args.has(n) } }; } /** * The dry run's own section for price changes of existing products: held unless the real run gets * --allow-price-changes. One line per product and region, old -> new. Empty when nothing is held. Pure. */ export function heldSection(held: HeldPrice[], allowed = false): string { if (!held.length) return ''; const head = `price changes on existing products (${allowed ? 'with --allow-price-changes: asked separately, each old -> new' : 'held: only with --allow-price-changes'}):`; return [head, ...held.flatMap(h => h.changes.map(c => ` ${h.id} ${c.region} ${c.from} -> ${c.to}`))].join('\n'); }