apple resubmit after a rejection: iap-submit reuses the open review submission (UNRESOLVED_ISSUES gets only missing items and is resubmitted, a draft is reused, one waiting for or in review means nothing is sent), never a second draft; 409 ITEM_PART_OF_ANOTHER_SUBMISSION names the other submission instead of the list-lag message, a draft the run created is canceled and the named one used; submit retried with backoff and heartbeat while Apple says not ready yet; new apple resubmit --build <n> (build by number, plan table, --yes); apple status lists UNRESOLVED_ISSUES items and the resubmit command. Pitfall 30
This commit is contained in:
parent
c8213be2b6
commit
0da5ee9d01
21 changed files with 726 additions and 80 deletions
|
|
@ -7,7 +7,7 @@
|
|||
import { json, type Call } from './store-mock';
|
||||
|
||||
export type R = { type: string; id: string; attributes: Record<string, unknown>; rels: Record<string, string | string[]>; order: number; hiddenReads?: number };
|
||||
export interface Fault { method: string; match: RegExp; when: 'before' | 'after'; status?: number; times?: number }
|
||||
export interface Fault { method: string; match: RegExp; when: 'before' | 'after'; status?: number; times?: number; code?: string; detail?: string }
|
||||
|
||||
const ASC = 'https://api.appstoreconnect.apple.com';
|
||||
const UPLOAD_HOST = 'https://upload.example.com/';
|
||||
|
|
@ -53,6 +53,7 @@ const LISTS: Record<string, { type: string; one?: boolean }> = {
|
|||
'appStoreVersionExperimentTreatments/appStoreVersionExperimentTreatmentLocalizations': { type: 'appStoreVersionExperimentTreatmentLocalizations' },
|
||||
'appStoreVersionExperimentTreatmentLocalizations/appScreenshotSets': { type: 'appScreenshotSets' },
|
||||
'reviewSubmissions/items': { type: 'reviewSubmissionItems' },
|
||||
'apps/reviewSubmissions': { type: 'reviewSubmissions' },
|
||||
'apps/buildUploads': { type: 'buildUploads' },
|
||||
'inAppPurchases/versions': { type: 'inAppPurchaseVersions' },
|
||||
'subscriptions/versions': { type: 'subscriptionVersions' },
|
||||
|
|
@ -155,6 +156,14 @@ export class FakeAsc {
|
|||
Array.isArray(v.data) ? (v.data as { id: string }[]).map(d => temp.get(d.id) ?? d.id) : temp.get((v.data as { id: string }).id) ?? (v.data as { id: string }).id]));
|
||||
const d = body.data;
|
||||
const main: Omit<R, 'order'> = { type: d.type, id: idFor(d.type), attributes: { ...DEFAULTS[d.type], ...(d.attributes ?? {}) }, rels: relsOf(d.relationships) };
|
||||
// App Store Connect keeps an item in one open review submission only
|
||||
if (d.type === 'reviewSubmissionItems') {
|
||||
const [kind, vid] = Object.entries(main.rels).find(([k]) => k !== 'reviewSubmission') ?? [];
|
||||
const holder = this.all('reviewSubmissionItems').find(r => r.rels[kind ?? ''] === vid && r.rels.reviewSubmission !== main.rels.reviewSubmission
|
||||
&& !['COMPLETE', 'CANCELING'].includes(String(this.res.get(String(r.rels.reviewSubmission))?.attributes.state)));
|
||||
if (holder) return json({ errors: [{ status: '409', code: 'STATE_ERROR.ITEM_PART_OF_ANOTHER_SUBMISSION', title: 'The item is already part of another submission',
|
||||
detail: `${this.res.get(String(vid))?.type ?? kind} with id ${vid} was already added to another reviewSubmission with id ${holder.rels.reviewSubmission}` }] }, 409);
|
||||
}
|
||||
const u = UNIQUE[d.type];
|
||||
if (u && this.all(d.type).some(r => u(r) === u(main as R)))
|
||||
return json({ errors: [{ status: '409', code: 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE', title: 'The provided entity includes an attribute with a value that has already been used', detail: 'already exists' }] }, 409);
|
||||
|
|
@ -252,6 +261,10 @@ export class FakeAsc {
|
|||
const { uploaded: _u, ...rest } = attrs;
|
||||
Object.assign(self.attributes, rest, { assetDeliveryState: { state: this.parts.includes(self.id) ? 'COMPLETE' : 'FAILED' } });
|
||||
delete self.attributes.uploadOperations;
|
||||
} else if (self.type === 'reviewSubmissions' && (attrs.submitted === true || attrs.canceled === true)) {
|
||||
// submitting moves the submission (and its items) on; canceling ends it
|
||||
self.attributes.state = attrs.submitted ? 'WAITING_FOR_REVIEW' : 'CANCELING';
|
||||
if (attrs.submitted) for (const it of this.children(self.id, 'reviewSubmissionItems')) it.attributes.state = 'READY_FOR_REVIEW';
|
||||
} else Object.assign(self.attributes, attrs);
|
||||
return json({ data: this.view(self) });
|
||||
}
|
||||
|
|
@ -413,7 +426,7 @@ export function installStores(): Stores {
|
|||
s.calls.push({ method, url, path, body });
|
||||
const f = s.faults.find(x => (x.times ?? 1) > 0 && x.method === method && x.match.test(decodeURIComponent(path)));
|
||||
if (f) f.times = (f.times ?? 1) - 1;
|
||||
if (f?.when === 'before') return json({ errors: [{ status: String(f.status ?? 503), detail: 'injected' }] }, f.status ?? 503);
|
||||
if (f?.when === 'before') return json({ errors: [{ status: String(f.status ?? 503), ...(f.code ? { code: f.code } : {}), detail: f.detail ?? 'injected' }] }, f.status ?? 503);
|
||||
let res: Response;
|
||||
if (url.startsWith(UPLOAD_HOST)) {
|
||||
partNo++;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue