play products: clamp regional prices to Play's min/max, per-product results; apple iap: skip live localizations, per-product results

This commit is contained in:
CRTkafa 2026-10-23 16:52:28 +08:00
parent 882255a32f
commit b0641c1fc8
12 changed files with 456 additions and 69 deletions

View file

@ -18,6 +18,7 @@
import { existsSync, readFileSync } from 'node:fs';
import { type OfferCodeDef, sellsOn, type Subscription, type SubscriptionGroup } from '../lib/config';
import type { Ctx } from '../lib/ctx';
import { isLockedLocState, isUnmodifiableError, lockedLocNote } from '../lib/loc-lock';
import { confirm, need } from '../lib/ctx';
import { done, log, step, warn } from '../lib/log';
import { allowPriceChanges, type HeldPrice, heldSection, holdPriceChanges, withoutYes } from '../lib/price-gate';
@ -73,7 +74,7 @@ const heldOf = (id: string, changes: { territory: string; from: number | null; t
/** Everything an existing subscription has that differs from the config (pure data; the run prints and applies it). */
export interface SubDrift {
attrs: AppleAttrDiff;
locs: { id: string; loc: string; patch: Record<string, string> }[];
locs: { id: string; loc: string; patch: Record<string, string>; locked?: string }[];
availability: { missing: string[]; newTerritories: boolean } | null;
intro: string[];
}
@ -88,7 +89,7 @@ export async function readSubDrift(c: Asc, s: Subscription, id: string, locale:
const loc = locale.get(lang);
const h = loc ? haveLocs.find((x: J) => x.attributes.locale === loc) : undefined;
const patch = h ? appleLocDiff(l, { name: h.attributes.name, description: h.attributes.description }) : null;
if (h && patch && loc) locs.push({ id: h.id, loc, patch });
if (h && patch && loc) locs.push({ id: h.id, loc, patch, ...(isLockedLocState(h.attributes.state) ? { locked: h.attributes.state as string } : {}) });
}
let availability: SubDrift['availability'] = null;
const av = (await c.maybe(`/v1/subscriptions/${id}/subscriptionAvailability`))?.data;
@ -103,14 +104,14 @@ export async function readSubDrift(c: Asc, s: Subscription, id: string, locale:
}
/** Group localizations whose name / customAppName differ from the config. Read-only. */
export async function readGroupLocDrift(c: Asc, g: SubscriptionGroup, groupId: string, locale: Map<string, string | undefined>): Promise<{ id: string; loc: string; patch: Record<string, string> }[]> {
export async function readGroupLocDrift(c: Asc, g: SubscriptionGroup, groupId: string, locale: Map<string, string | undefined>): Promise<{ id: string; loc: string; patch: Record<string, string>; locked?: string }[]> {
const have = await c.all(`/v1/subscriptionGroups/${groupId}/subscriptionGroupLocalizations?limit=200`);
const out: { id: string; loc: string; patch: Record<string, string> }[] = [];
const out: { id: string; loc: string; patch: Record<string, string>; locked?: string }[] = [];
for (const [lang, l] of Object.entries(g.listings)) {
const loc = locale.get(lang);
const h = loc ? have.find((x: J) => x.attributes.locale === loc) : undefined;
const patch = h ? appleGroupLocDiff(l, { name: h.attributes.name, customAppName: h.attributes.customAppName }) : null;
if (h && patch && loc) out.push({ id: h.id, loc, patch });
if (h && patch && loc) out.push({ id: h.id, loc, patch, ...(isLockedLocState(h.attributes.state) ? { locked: h.attributes.state as string } : {}) });
}
return out;
}
@ -120,7 +121,7 @@ export function driftLines(id: string, d: SubDrift): string[] {
const av = d.availability;
return [
...d.attrs.text.map(x => `${id}: ${x}`), ...d.attrs.report,
...d.locs.map(l => `${id} ${l.loc}: ${Object.keys(l.patch).join(' + ')} differ from the config`),
...d.locs.map(l => `${id} ${l.loc}: ${Object.keys(l.patch).join(' + ')} differ from the config${l.locked ? ` - ${lockedLocNote(l.locked)}` : ''}`),
...(av ? [`${id}: availability${av.missing.length ? ` ${av.missing.length} territories missing (${av.missing.slice(0, 5).join(', ')})` : ''}${av.newTerritories ? ' new territories off' : ''}`] : []),
...d.intro,
];
@ -160,7 +161,7 @@ export async function run(ctx: Ctx): Promise<void> {
const heldDry: HeldPrice[] = [];
for (const g of groups) {
const sg = state.groups.find(x => x.referenceName === g.name);
if (sg) for (const d of await readGroupLocDrift(c, g, sg.id, locale)) log(TAG, `group "${g.name}" ${d.loc}: ${Object.keys(d.patch).join(' + ')} differ from the config`);
if (sg) for (const d of await readGroupLocDrift(c, g, sg.id, locale)) log(TAG, `group "${g.name}" ${d.loc}: ${Object.keys(d.patch).join(' + ')} differ from the config${d.locked ? ` - ${lockedLocNote(d.locked)}` : ''}`);
for (const s of g.subscriptions) {
const cur = state.subscriptions.find(x => x.productId === s.id);
if (!cur) continue;
@ -209,7 +210,11 @@ export async function run(ctx: Ctx): Promise<void> {
}
if (state.groups.some(x => x.id === groupId)) {
for (const d of await readGroupLocDrift(c, g, groupId, locale)) {
await c.asc('PATCH', `/v1/subscriptionGroupLocalizations/${d.id}`, appleGroupLocPatchBody(d.id, d.patch));
if (d.locked) { warn(TAG, ` group ${d.loc}: ${lockedLocNote(d.locked)}; skipped`); continue; }
try { await c.asc('PATCH', `/v1/subscriptionGroupLocalizations/${d.id}`, appleGroupLocPatchBody(d.id, d.patch)); } catch (e) {
if (!isUnmodifiableError(e)) throw e;
warn(TAG, ` group ${d.loc}: ${lockedLocNote('ACTIVE')}; skipped`); continue;
}
log(TAG, ` group ${d.loc}: ${Object.keys(d.patch).join(' + ')} updated`);
}
}
@ -242,7 +247,14 @@ export async function run(ctx: Ctx): Promise<void> {
// and the period / family sharing are only reported (Apple cannot change them)
const d = await readSubDrift(c, s, subId, locale, territories);
if (Object.keys(d.attrs.patch).length) { await c.asc('PATCH', `/v1/subscriptions/${subId}`, appleSubPatchBody(subId, d.attrs.patch)); log(TAG, ` attributes updated: ${d.attrs.text.join(', ')}`); }
for (const l of d.locs) { await c.asc('PATCH', `/v1/subscriptionLocalizations/${l.id}`, appleLocPatchBody(l.id, l.patch)); log(TAG, ` ${l.loc}: ${Object.keys(l.patch).join(' + ')} updated`); }
for (const l of d.locs) {
if (l.locked) { warn(TAG, ` ${l.loc}: ${lockedLocNote(l.locked)}; skipped`); continue; }
try { await c.asc('PATCH', `/v1/subscriptionLocalizations/${l.id}`, appleLocPatchBody(l.id, l.patch)); } catch (e) {
if (!isUnmodifiableError(e)) throw e;
warn(TAG, ` ${l.loc}: ${lockedLocNote('ACTIVE')}; skipped`); continue;
}
log(TAG, ` ${l.loc}: ${Object.keys(l.patch).join(' + ')} updated`);
}
if (d.availability) { await c.asc('POST', '/v1/subscriptionAvailabilities', appleAvailabilityBody(subId, territories)); log(TAG, ` availability set again: ${territories.length} territories + new ones`); }
for (const x of [...d.attrs.report, ...d.intro]) warn(TAG, x);
}