@@ -467,8 +467,8 @@ router.get("/:appId/builds", async (req, res) => {
467467 const account = accounts . find ( ( a ) => a . id === accountId ) || accounts [ 0 ] ;
468468
469469 try {
470- const fields = "fields[builds]=version,processingState,uploadedDate,iconAssetToken,minOsVersion,buildAudienceType" ;
471- const encryptionInclude = "include=appEncryptionDeclaration&fields[appEncryptionDeclarations]=usesNonExemptEncryption, appEncryptionDeclarationState" ;
470+ const fields = "fields[builds]=version,processingState,uploadedDate,iconAssetToken,minOsVersion,buildAudienceType,usesNonExemptEncryption " ;
471+ const encryptionInclude = "include=appEncryptionDeclaration&fields[appEncryptionDeclarations]=appEncryptionDeclarationState" ;
472472 let url ;
473473 if ( versionString ) {
474474 url = `/v1/builds?filter[app]=${ appId } &filter[preReleaseVersion.version]=${ encodeURIComponent ( versionString ) } &${ fields } &${ encryptionInclude } &limit=25` ;
@@ -497,6 +497,11 @@ router.get("/:appId/builds", async (req, res) => {
497497 }
498498 const declId = b . relationships ?. appEncryptionDeclaration ?. data ?. id || null ;
499499 const declAttrs = declId ? includedDeclarations . get ( declId ) : null ;
500+ // Derive compliance: if usesNonExemptEncryption is explicitly false, compliance is resolved
501+ let complianceState = declAttrs ?. appEncryptionDeclarationState ?? null ;
502+ if ( ! complianceState && attrs . usesNonExemptEncryption === false ) {
503+ complianceState = "VALID" ;
504+ }
500505 return {
501506 id : b . id ,
502507 version : attrs . version ,
@@ -506,8 +511,8 @@ router.get("/:appId/builds", async (req, res) => {
506511 buildAudienceType : attrs . buildAudienceType ,
507512 iconUrl,
508513 encryptionDeclarationId : declId ,
509- usesNonExemptEncryption : declAttrs ? .usesNonExemptEncryption ?? null ,
510- complianceState : declAttrs ?. appEncryptionDeclarationState ?? null ,
514+ usesNonExemptEncryption : attrs . usesNonExemptEncryption ?? null ,
515+ complianceState,
511516 } ;
512517 } ) . sort ( ( a , b ) => new Date ( b . uploadedDate ) - new Date ( a . uploadedDate ) ) ;
513518
@@ -533,7 +538,7 @@ router.get("/:appId/versions/:versionId/build", async (req, res) => {
533538 try {
534539 const data = await ascFetch (
535540 account ,
536- `/v1/appStoreVersions/${ versionId } /build?fields[builds]=version,processingState,uploadedDate,minOsVersion`
541+ `/v1/appStoreVersions/${ versionId } /build?fields[builds]=version,processingState,uploadedDate,minOsVersion,usesNonExemptEncryption `
537542 ) ;
538543
539544 const build = data . data
@@ -543,6 +548,7 @@ router.get("/:appId/versions/:versionId/build", async (req, res) => {
543548 processingState : data . data . attributes . processingState ,
544549 uploadedDate : data . data . attributes . uploadedDate ,
545550 minOsVersion : data . data . attributes . minOsVersion ,
551+ usesNonExemptEncryption : data . data . attributes . usesNonExemptEncryption ?? null ,
546552 }
547553 : null ;
548554
@@ -609,7 +615,7 @@ router.get("/:appId/builds/:buildId/encryptionDeclaration", async (req, res) =>
609615 try {
610616 const data = await ascFetch (
611617 account ,
612- `/v1/builds/${ buildId } /appEncryptionDeclaration?fields[appEncryptionDeclarations]=usesNonExemptEncryption, appEncryptionDeclarationState,containsProprietaryCryptography,containsThirdPartyCryptography,availableOnFrenchStore,codeValue,platform`
618+ `/v1/builds/${ buildId } /appEncryptionDeclaration?fields[appEncryptionDeclarations]=appEncryptionDeclarationState,containsProprietaryCryptography,containsThirdPartyCryptography,availableOnFrenchStore,codeValue,platform`
613619 ) ;
614620
615621 const decl = data . data
@@ -630,7 +636,7 @@ router.get("/:appId/builds/:buildId/encryptionDeclaration", async (req, res) =>
630636
631637router . patch ( "/:appId/builds/:buildId/encryptionDeclaration" , async ( req , res ) => {
632638 const { appId, buildId } = req . params ;
633- const { accountId, usesNonExemptEncryption , containsProprietaryCryptography, containsThirdPartyCryptography } = req . body ;
639+ const { accountId, containsProprietaryCryptography, containsThirdPartyCryptography } = req . body ;
634640
635641 if ( ! accountId ) {
636642 return res . status ( 400 ) . json ( { error : "accountId is required" } ) ;
@@ -643,36 +649,61 @@ router.patch("/:appId/builds/:buildId/encryptionDeclaration", async (req, res) =
643649 }
644650
645651 try {
646- // Fetch the existing declaration ID
647- const declData = await ascFetch (
648- account ,
649- `/v1/builds/${ buildId } /appEncryptionDeclaration?fields[appEncryptionDeclarations]=appEncryptionDeclarationState`
650- ) ;
651-
652- if ( ! declData . data ) {
653- return res . status ( 404 ) . json ( { error : "No encryption declaration found for this build" } ) ;
654- }
655-
656- const declarationId = declData . data . id ;
657- const attributes = { usesNonExemptEncryption } ;
658- if ( usesNonExemptEncryption ) {
659- if ( containsProprietaryCryptography !== undefined ) attributes . containsProprietaryCryptography = containsProprietaryCryptography ;
660- if ( containsThirdPartyCryptography !== undefined ) attributes . containsThirdPartyCryptography = containsThirdPartyCryptography ;
661- }
652+ const usesEncryption = containsProprietaryCryptography || containsThirdPartyCryptography ;
662653
663- await ascFetch ( account , `/v1/appEncryptionDeclarations/${ declarationId } ` , {
664- method : "PATCH" ,
665- body : {
666- data : {
667- type : "appEncryptionDeclarations" ,
668- id : declarationId ,
669- attributes,
654+ if ( ! usesEncryption ) {
655+ // "No encryption" — set usesNonExemptEncryption=false on the build directly
656+ await ascFetch ( account , `/v1/builds/${ buildId } ` , {
657+ method : "PATCH" ,
658+ body : {
659+ data : {
660+ type : "builds" ,
661+ id : buildId ,
662+ attributes : { usesNonExemptEncryption : false } ,
663+ } ,
670664 } ,
671- } ,
672- } ) ;
665+ } ) ;
666+ } else {
667+ // "Yes encryption" — create a declaration and link it to the build
668+ const declAttrs = {
669+ containsProprietaryCryptography,
670+ containsThirdPartyCryptography,
671+ availableOnFrenchStore : false ,
672+ appDescription : "N/A" ,
673+ } ;
674+ const created = await ascFetch ( account , `/v1/appEncryptionDeclarations` , {
675+ method : "POST" ,
676+ body : {
677+ data : {
678+ type : "appEncryptionDeclarations" ,
679+ attributes : declAttrs ,
680+ relationships : {
681+ app : { data : { type : "apps" , id : appId } } ,
682+ } ,
683+ } ,
684+ } ,
685+ } ) ;
686+ // Link declaration to build via PATCH on the build
687+ await ascFetch ( account , `/v1/builds/${ buildId } ` , {
688+ method : "PATCH" ,
689+ body : {
690+ data : {
691+ type : "builds" ,
692+ id : buildId ,
693+ attributes : { usesNonExemptEncryption : true } ,
694+ relationships : {
695+ appEncryptionDeclaration : {
696+ data : { type : "appEncryptionDeclarations" , id : created . data . id } ,
697+ } ,
698+ } ,
699+ } ,
700+ } ,
701+ } ) ;
702+ }
673703
674704 apiCache . deleteByPrefix ( `apps:build-encryption:${ buildId } :` ) ;
675705 apiCache . deleteByPrefix ( `apps:builds:${ appId } :` ) ;
706+ apiCache . deleteByPrefix ( `apps:version-build:` ) ;
676707
677708 res . json ( { success : true } ) ;
678709 } catch ( err ) {
@@ -959,6 +990,180 @@ router.get("/:appId/review-submissions", async (req, res) => {
959990 }
960991} ) ;
961992
993+ // ── Review Submission Detail ──────────────────────────────────────────────
994+
995+ const ITEM_STATE_DISPLAY = {
996+ READY_FOR_REVIEW : "Ready for Review" ,
997+ ACCEPTED : "Accepted" ,
998+ APPROVED : "Approved" ,
999+ REJECTED : "Rejected" ,
1000+ REMOVED : "Removed" ,
1001+ } ;
1002+
1003+ function parseReviewSubmissionDetail ( data ) {
1004+ const includedMap = new Map ( ) ;
1005+ if ( data . included ) {
1006+ for ( const inc of data . included ) {
1007+ includedMap . set ( `${ inc . type } :${ inc . id } ` , inc ) ;
1008+ }
1009+ }
1010+
1011+ const submission = data . data ;
1012+ const attrs = submission . attributes ;
1013+
1014+ // Resolve version string
1015+ let versions = null ;
1016+ const versionRef = submission . relationships ?. appStoreVersionForReview ?. data ;
1017+ if ( versionRef ) {
1018+ const ver = includedMap . get ( `${ versionRef . type } :${ versionRef . id } ` ) ;
1019+ if ( ver ) {
1020+ const platform = ver . attributes . platform === "IOS" ? "iOS" : ver . attributes . platform === "MAC_OS" ? "macOS" : ver . attributes . platform ;
1021+ versions = `${ platform } ${ ver . attributes . versionString } ` ;
1022+ }
1023+ }
1024+
1025+ // Resolve actors
1026+ function resolveActor ( relationshipName ) {
1027+ const ref = submission . relationships ?. [ relationshipName ] ?. data ;
1028+ if ( ! ref ) return null ;
1029+ const actor = includedMap . get ( `${ ref . type } :${ ref . id } ` ) ;
1030+ if ( ! actor ) return null ;
1031+ return [ actor . attributes . userFirstName , actor . attributes . userLastName ] . filter ( Boolean ) . join ( " " ) ;
1032+ }
1033+
1034+ // Resolve items
1035+ const itemRefs = submission . relationships ?. items ?. data || [ ] ;
1036+ const items = itemRefs . map ( ( ref ) => {
1037+ const item = includedMap . get ( `${ ref . type } :${ ref . id } ` ) ;
1038+ if ( ! item ) return { id : ref . id , state : "UNKNOWN" , displayState : "Unknown" , type : "unknown" } ;
1039+
1040+ const itemState = item . attributes . state ;
1041+ const itemType = [ "appStoreVersion" , "appCustomProductPage" , "appStoreVersionExperiment" , "appEvent" ]
1042+ . find ( ( rel ) => item . relationships ?. [ rel ] ?. data ) || "appStoreVersion" ;
1043+
1044+ let versionString = null ;
1045+ let itemPlatform = null ;
1046+ let appStoreState = null ;
1047+ const itemVersionRef = item . relationships ?. appStoreVersion ?. data ;
1048+ if ( itemVersionRef ) {
1049+ const ver = includedMap . get ( `${ itemVersionRef . type } :${ itemVersionRef . id } ` ) ;
1050+ if ( ver ) {
1051+ versionString = ver . attributes . versionString ;
1052+ itemPlatform = ver . attributes . platform === "IOS" ? "iOS" : ver . attributes . platform === "MAC_OS" ? "macOS" : ver . attributes . platform ;
1053+ appStoreState = ver . attributes . appStoreState ;
1054+ }
1055+ }
1056+
1057+ // If no version from items, fall back to the submission-level version
1058+ if ( ! versionString && versionRef ) {
1059+ const ver = includedMap . get ( `${ versionRef . type } :${ versionRef . id } ` ) ;
1060+ if ( ver ) {
1061+ versionString = ver . attributes . versionString ;
1062+ itemPlatform = ver . attributes . platform === "IOS" ? "iOS" : ver . attributes . platform === "MAC_OS" ? "macOS" : ver . attributes . platform ;
1063+ appStoreState = ver . attributes . appStoreState ;
1064+ }
1065+ }
1066+
1067+ return {
1068+ id : item . id ,
1069+ state : itemState ,
1070+ displayState : ITEM_STATE_DISPLAY [ itemState ] || itemState ,
1071+ type : itemType ,
1072+ versionString,
1073+ platform : itemPlatform ,
1074+ appStoreState,
1075+ } ;
1076+ } ) ;
1077+
1078+ // If no version resolved yet, try from items
1079+ if ( ! versions && items . length > 0 ) {
1080+ const versionStrings = new Set ( items . filter ( ( i ) => i . versionString && i . platform ) . map ( ( i ) => `${ i . platform } ${ i . versionString } ` ) ) ;
1081+ if ( versionStrings . size > 1 ) versions = "Multiple Versions" ;
1082+ else if ( versionStrings . size === 1 ) versions = [ ...versionStrings ] [ 0 ] ;
1083+ }
1084+
1085+ const displayStatus = REVIEW_STATE_DISPLAY [ attrs . state ] || attrs . state ;
1086+ const platform = attrs . platform === "IOS" ? "iOS" : attrs . platform === "MAC_OS" ? "macOS" : attrs . platform ;
1087+
1088+ return {
1089+ id : submission . id ,
1090+ state : attrs . state ,
1091+ displayStatus,
1092+ platform,
1093+ submittedDate : attrs . submittedDate ,
1094+ versions : versions || "Unknown" ,
1095+ submittedBy : resolveActor ( "submittedByActor" ) ,
1096+ lastUpdatedBy : resolveActor ( "lastUpdatedByActor" ) ,
1097+ items,
1098+ } ;
1099+ }
1100+
1101+ router . get ( "/:appId/review-submissions/:submissionId" , async ( req , res ) => {
1102+ const { appId, submissionId } = req . params ;
1103+ const { accountId } = req . query ;
1104+
1105+ const cacheKey = `apps:review-submission-detail:${ submissionId } :${ accountId || "default" } ` ;
1106+ const cached = apiCache . get ( cacheKey ) ;
1107+ if ( cached ) return res . json ( cached ) ;
1108+
1109+ const accounts = getAccounts ( ) ;
1110+ const account = accounts . find ( ( a ) => a . id === accountId ) || accounts [ 0 ] ;
1111+
1112+ try {
1113+ const submissionUrl = `/v1/reviewSubmissions/${ submissionId } `
1114+ + "?include=items,appStoreVersionForReview,submittedByActor,lastUpdatedByActor"
1115+ + "&fields[reviewSubmissions]=submittedDate,state,platform"
1116+ + "&fields[reviewSubmissionItems]=state,appStoreVersion"
1117+ + "&fields[appStoreVersions]=versionString,platform,appStoreState"
1118+ + "&fields[actors]=userFirstName,userLastName" ;
1119+
1120+ const itemsUrl = `/v1/reviewSubmissions/${ submissionId } /items`
1121+ + "?include=appStoreVersion"
1122+ + "&fields[reviewSubmissionItems]=state,appStoreVersion"
1123+ + "&fields[appStoreVersions]=versionString,platform,appStoreState" ;
1124+
1125+ const [ submissionData , itemsData ] = await Promise . all ( [
1126+ ascFetch ( account , submissionUrl ) ,
1127+ ascFetch ( account , itemsUrl ) ,
1128+ ] ) ;
1129+
1130+ // Replace item objects with richer ones from items endpoint (they carry appStoreVersion relationship)
1131+ if ( ! submissionData . included ) submissionData . included = [ ] ;
1132+ if ( itemsData . data ) {
1133+ for ( const item of itemsData . data ) {
1134+ const idx = submissionData . included . findIndex ( ( e ) => e . type === item . type && e . id === item . id ) ;
1135+ if ( idx >= 0 ) submissionData . included [ idx ] = item ;
1136+ else submissionData . included . push ( item ) ;
1137+ }
1138+ }
1139+ // Merge included version objects from items endpoint
1140+ if ( itemsData . included ) {
1141+ for ( const inc of itemsData . included ) {
1142+ const exists = submissionData . included . some ( ( e ) => e . type === inc . type && e . id === inc . id ) ;
1143+ if ( ! exists ) submissionData . included . push ( inc ) ;
1144+ }
1145+ }
1146+
1147+ // DEBUG: log raw API responses to understand structure
1148+ console . log ( "=== SUBMISSION DATA ===" ) ;
1149+ console . log ( "data.relationships:" , JSON . stringify ( submissionData . data ?. relationships , null , 2 ) ) ;
1150+ console . log ( "included types:" , submissionData . included ?. map ( i => `${ i . type } :${ i . id } ` ) ) ;
1151+ console . log ( "=== ITEMS DATA ===" ) ;
1152+ console . log ( "itemsData.data:" , JSON . stringify ( itemsData . data , null , 2 ) ) ;
1153+ console . log ( "itemsData.included:" , JSON . stringify ( itemsData . included , null , 2 ) ) ;
1154+ console . log ( "=== MERGED INCLUDED ===" ) ;
1155+ console . log ( "all included:" , submissionData . included ?. map ( i => `${ i . type } :${ i . id } rels=${ Object . keys ( i . relationships || { } ) . join ( "," ) } ` ) ) ;
1156+
1157+ const result = parseReviewSubmissionDetail ( submissionData ) ;
1158+
1159+ apiCache . set ( cacheKey , result ) ;
1160+ res . json ( result ) ;
1161+ } catch ( err ) {
1162+ console . error ( `Failed to fetch review submission detail ${ submissionId } :` , err . message ) ;
1163+ res . status ( 502 ) . json ( { error : err . message } ) ;
1164+ }
1165+ } ) ;
1166+
9621167router . post ( "/:appId/versions/:versionId/submit" , async ( req , res ) => {
9631168 const { appId, versionId } = req . params ;
9641169 const { accountId, platform } = req . body ;
0 commit comments