onboard never writes duplicate subscription levels: the App Store groupLevel when it is set and unique in the group, else a stable order (App Store level, higher price per month = higher level, longer period, id) with a note per derived level; assignLevels in cli/lib/onboard.ts, tests with a live-shape App Store group (groupLevel 2 and 1) and Play-only / tied cases

This commit is contained in:
CRTkafa 2026-10-14 05:19:15 +08:00
parent 346bf61166
commit 5ad3c0002d
2 changed files with 80 additions and 1 deletions

View file

@ -55,6 +55,8 @@ export function importCatalog(config: Pick<ShipConfig, 'languages' | 'products'
const oldSubs = new Map((config.subscriptions ?? []).flatMap(g => g.subscriptions.map(x => [x.id, { group: g, sub: x }] as const)));
const ids = [...new Set([...A.keys(), ...P.keys()])];
const groups = new Map<string, SubscriptionGroup>();
/** App Store groupLevel per subscription id (the only store that proves a level) */
const appleLevel = new Map<string, number>();
for (const id of ids) {
const a = A.get(id), p = P.get(id);
@ -100,6 +102,7 @@ export function importCatalog(config: Pick<ShipConfig, 'languages' | 'products'
if (!period || !PERIODS.includes(period)) { out.skipped.push({ id, why: `billing period ${period ?? 'unknown'} is not one the config supports (${PERIODS.join(', ')})` }); continue; }
if (a?.period && p?.period && a.period !== p.period) note(id, `bills every ${a.period} on the App Store, ${p.period} on Google Play: period follows the App Store`, 'warn');
const prev = oldSubs.get(id);
if (typeof a?.level === 'number') appleLevel.set(id, a.level);
const sub: Subscription = { ...(prev?.sub ?? {}), id, period, level: a?.level ?? prev?.sub.level ?? 1, usd: cents(usd), listings, ...platforms,
...(overrides.length || prev?.sub.overrides ? { overrides: mergeOverrides(prev?.sub.overrides, overrides) } : {}) } as Subscription;
if (!both || (a && p)) delete sub.platforms;
@ -135,9 +138,39 @@ export function importCatalog(config: Pick<ShipConfig, 'languages' | 'products'
note(x.id, 'in the config, on no store yet: kept');
}
out.subscriptions = [...groups.values()];
for (const g of out.subscriptions) for (const n of assignLevels(g, appleLevel)) note(n.id, n.msg, n.level);
return out;
}
const PERIOD_MONTHS: Record<SubscriptionPeriod, number> = { P1W: 12 / 52, P1M: 1, P2M: 2, P3M: 3, P6M: 6, P1Y: 12 };
/**
* Levels inside one group (1 = highest level of service), never duplicated (config validation refuses duplicates).
* The App Store's groupLevel when it proves every level in the group without ties; otherwise a stable order: App
* Store level first, then the higher price per month, then the longer period, then the id. Mutates the group.
*/
export function assignLevels(g: SubscriptionGroup, appleLevel: Map<string, number>): ImportNote[] {
const subs = g.subscriptions;
const levels = subs.map(x => appleLevel.get(x.id));
if (levels.every(l => l !== undefined) && new Set(levels).size === subs.length) {
for (const x of subs) x.level = appleLevel.get(x.id)!;
return [];
}
const monthly = (x: Subscription) => x.usd / PERIOD_MONTHS[x.period];
const order = [...subs].sort((x, y) => (appleLevel.get(x.id) ?? Infinity) - (appleLevel.get(y.id) ?? Infinity)
|| monthly(y) - monthly(x) || PERIOD_MONTHS[y.period] - PERIOD_MONTHS[x.period] || x.id.localeCompare(y.id));
const notes: ImportNote[] = [];
order.forEach((x, i) => {
const al = appleLevel.get(x.id);
const how = al === undefined
? 'no App Store groupLevel (Google Play has no levels): ordered by price per month (higher price = higher level, level 1 the highest), then the longer period'
: `App Store groupLevel ${al} is shared with another subscription in the group: ties ordered by price per month (higher first), then the longer period`;
if (al !== i + 1) notes.push({ id: x.id, level: 'warn', msg: `group "${g.name}": level ${i + 1}, ${how}; check it in App Store Connect` });
x.level = i + 1;
});
return notes;
}
function mergeOverrides(prev: { region: string; currency: string; price: number; territory?: string }[] | undefined, add: { region: string; currency: string; price: number }[]) {
const m = new Map((prev ?? []).map(o => [o.region, o] as const));
for (const o of add) m.set(o.region, { ...m.get(o.region), ...o });