// 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 { 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 { const r = await findOrCreate({ 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 = { 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 { 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 { 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; }