Resubmit a rejected version through its open submission

When App Review rejects a version, its review submission stays open as UNRESOLVED_ISSUES and still holds the version, while the version itself reads REJECTED or READY_FOR_REVIEW. Resubmitting means submitting that same submission again. plan_submission only looked for READY_FOR_REVIEW drafts, so for a rejected version it planned a new submission, which App Store Connect refuses while the old one is open. The lookup now uses OPEN_STATES (READY_FOR_REVIEW, UNRESOLVED_ISSUES), and the plan reports which state the reused submission is in. A release readiness pass caught this on Gt8 iOS 1.0.5, Gt8 macOS 1.0.6 and KaroKowe visionOS 1.0.1. All three now plan to reuse their UNRESOLVED_ISSUES submission, which already holds the version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Seto Elkahfi committed Oct 9, 2026 at 11:14 UTC 6cede30a81be8c1dfd819302e980965e37d28abb
2 files changed +38 -4
crates/aso/src/review_submission.rs
+26
@@ -26,6 +26,14 @@ pub const ITEM_RESOURCE_TYPE: &str = "reviewSubmissionItems";
26 /// `COMPLETING`, or `COMPLETE`.
27 pub const STATE_READY_FOR_REVIEW: &str = "READY_FOR_REVIEW";
28
29 +/// A submission App Review sent back. It stays open, still holding its
30 +/// items, and resubmitting means submitting this same one again.
31 +pub const STATE_UNRESOLVED_ISSUES: &str = "UNRESOLVED_ISSUES";
32 +
33 +/// States of a submission that is still open for this app and platform,
34 +/// so a new submission can't be created alongside it.
35 +pub const OPEN_STATES: [&str; 2] = [STATE_READY_FOR_REVIEW, STATE_UNRESOLVED_ISSUES];
36 +
37 #[derive(Debug, Clone, Serialize, Deserialize)]
38 #[serde(rename_all = "camelCase")]
39 pub struct ReviewSubmissionAttributes {
@@ -276,6 +284,24 @@ mod tests {
284 );
285 }
286
287 + /// A rejected version's submission is `UNRESOLVED_ISSUES`, not
288 + /// `READY_FOR_REVIEW`, so looking only for drafts misses it and the
289 + /// caller tries to create a second submission, which App Store
290 + /// Connect refuses.
291 + #[test]
292 + fn open_states_include_a_submission_app_review_sent_back() {
293 + let states = OPEN_STATES.join(",");
294 + assert_eq!(
295 + list_query("6756177644", Some(Platform::Ios), &states),
296 + vec![
297 + ("filter[app]", "6756177644"),
298 + ("limit", "200"),
299 + ("filter[platform]", "IOS"),
300 + ("filter[state]", "READY_FOR_REVIEW,UNRESOLVED_ISSUES"),
301 + ]
302 + );
303 + }
304 +
305 #[test]
306 fn create_body_names_the_platform_and_app() {
307 let body = CreateBody {
crates/frontend/src/review.rs
+12 -4
@@ -8,7 +8,7 @@
8 use serde::Serialize;
9 use smbcloud_ascapi_aso::app_store_version::Platform;
10 use smbcloud_ascapi_aso::prelude::*;
11 -use smbcloud_ascapi_aso::review_submission::STATE_READY_FOR_REVIEW;
11 +use smbcloud_ascapi_aso::review_submission::OPEN_STATES;
12 use smbcloud_ascapi_core::Client;
13
14 /// What [`submit_for_review`] will do, worked out from reads only.
@@ -19,9 +19,13 @@ pub struct SubmissionPlan {
19 pub version_string: Option<String>,
20 pub platform: String,
21 pub app_version_state: Option<String>,
22 - /// An open (`READY_FOR_REVIEW`) submission for this app and platform to
23 - /// reuse. `None` means a new one gets created.
22 + /// An open submission for this app and platform to reuse: a draft
23 + /// (`READY_FOR_REVIEW`), or one App Review sent back
24 + /// (`UNRESOLVED_ISSUES`), which is how a rejected version is
25 + /// resubmitted. `None` means a new one gets created.
26 pub existing_submission_id: Option<String>,
27 + /// That submission's state, when there is one.
28 + pub existing_submission_state: Option<String>,
29 /// Whether the open submission already carries an item. App Store
30 /// Connect allows one item per version, so when this is true the item
31 /// step is skipped.
@@ -52,10 +56,13 @@ pub async fn plan_submission(
56 let platform = version.attributes.platform;
57
58 let open = client
55 - .list_review_submissions(app_id, Some(platform), &[STATE_READY_FOR_REVIEW])
59 + .list_review_submissions(app_id, Some(platform), &OPEN_STATES)
60 .await
61 .map_err(|error| error.to_string())?;
62 let existing = open.into_iter().next();
63 + let existing_submission_state = existing
64 + .as_ref()
65 + .and_then(|submission| submission.attributes.state.clone());
66
67 let existing_submission_has_items = match &existing {
68 Some(submission) => !client
@@ -80,6 +87,7 @@ pub async fn plan_submission(
87 platform: platform_name(platform).to_string(),
88 app_version_state: version.attributes.app_version_state,
89 existing_submission_id: existing.map(|submission| submission.id),
90 + existing_submission_state,
91 existing_submission_has_items,
92 })
93 }