storebound/cli/apple/submission.ts
2026-10-12 04:00:58 +08:00

47 lines
3.3 KiB
TypeScript

// The app's one draft review submission and the items in it, without duplicates: a draft (READY_FOR_REVIEW) is
// reused, a new one is created only after a fresh look (Apple keeps one open draft per app and platform), and an item
// already in the draft is not added again (a 409 or a re-read finds it). Used by iap-submit, cpp submit and events
// submit. Submitting itself (PATCH submitted: true) stays the owner's confirmed step in those commands.
import { log } from '../lib/log';
import { createOnce, findOrCreate } from '../lib/upsert';
import type { Asc, J } from './asc';
import { one } from './jsonapi';
/** The draft review submission of the app (READY_FOR_REVIEW, iOS), or null. */
export async function findDraft(c: Asc, appId: string): Promise<string | null> {
return (await c.all(`/v1/reviewSubmissions?filter[app]=${appId}&filter[platform]=IOS&filter[state]=READY_FOR_REVIEW&limit=10`))[0]?.id ?? null;
}
/** The draft's id: the existing one, else a new draft created once. */
export async function draftSubmission(c: Asc, appId: string, root: string, tag: string): Promise<string> {
const r = await findOrCreate<string>({ what: 'draft review submission', tag, unique: true,
find: () => findDraft(c, appId),
journal: { root, key: `apple:${appId}:reviewSubmission:draft`, idOf: id => id,
byId: async id => { const d = (await c.maybe(`/v1/reviewSubmissions/${id}`))?.data; return d?.attributes?.state === 'READY_FOR_REVIEW' ? id : null; } },
create: async () => (await c.asc('POST', '/v1/reviewSubmissions', { data: { type: 'reviewSubmissions', attributes: { platform: 'IOS' }, relationships: { app: one('apps', appId) } } })).data.id as string });
if (r.created) log(tag, `draft review submission ${r.value} created`);
return r.value;
}
const TYPE: Record<string, string> = {
appStoreVersion: 'appStoreVersions', inAppPurchaseVersion: 'inAppPurchaseVersions', subscriptionVersion: 'subscriptionVersions',
subscriptionGroupVersion: 'subscriptionGroupVersions', appCustomProductPageVersion: 'appCustomProductPageVersions', appEvent: 'appEvents',
appStoreVersionExperiment: 'appStoreVersionExperiments',
};
/** Whether the draft already holds `id` as `kind` (e.g. inAppPurchaseVersion). */
export async function inDraft(c: Asc, sid: string, kind: string, id: string): Promise<boolean> {
const items = await c.all(`/v1/reviewSubmissions/${sid}/items?include=${kind}&limit=200`);
return items.some((it: J) => it.relationships?.[kind]?.data?.id === id);
}
/** Adds one item to the draft unless it is in there already (`checked`: the caller read the draft's items a moment
* ago). true when it was added now. */
export async function addItem(c: Asc, sid: string, kind: string, id: string, label: string, tag: string, o: { checked?: boolean } = {}): Promise<boolean> {
if (!o.checked && await inDraft(c, sid, kind, id)) { log(tag, `${label}: already in draft ${sid}`); return false; }
const r = await createOnce({ what: `${label} in draft ${sid}`, tag, unique: true,
find: async () => ((await inDraft(c, sid, kind, id)) ? id : null),
create: async () => (await c.asc('POST', '/v1/reviewSubmissionItems', { data: { type: 'reviewSubmissionItems',
relationships: { reviewSubmission: one('reviewSubmissions', sid), [kind]: one(TYPE[kind] ?? `${kind}s`, id) } } })).data?.id ?? id });
return r.created;
}