@@ -27,6 +27,7 @@ import {
2727} from '@adobe/spacecat-shared-utils' ;
2828
2929import { ValidationError , Suggestion as SuggestionModel , Site as SiteModel } from '@adobe/spacecat-shared-data-access' ;
30+ import TokowakaClient from '@adobe/spacecat-shared-tokowaka-client' ;
3031import { SuggestionDto } from '../dto/suggestion.js' ;
3132import { FixDto } from '../dto/fix.js' ;
3233import { sendAutofixMessage , getCSPromiseToken , ErrorWithStatusCode } from '../support/utils.js' ;
@@ -778,9 +779,158 @@ function SuggestionsController(ctx, sqs, env) {
778779 }
779780 } ;
780781
782+ /**
783+ * Deploys suggestions through Tokowaka edge delivery
784+ * @param {Object } context of the request
785+ * @returns {Promise<Response> } Deployment response
786+ */
787+ const deploySuggestionToEdge = async ( context ) => {
788+ const siteId = context . params ?. siteId ;
789+ const opportunityId = context . params ?. opportunityId ;
790+
791+ if ( ! isValidUUID ( siteId ) ) {
792+ return badRequest ( 'Site ID required' ) ;
793+ }
794+
795+ if ( ! isValidUUID ( opportunityId ) ) {
796+ return badRequest ( 'Opportunity ID required' ) ;
797+ }
798+
799+ // validate request body
800+ if ( ! isNonEmptyObject ( context . data ) ) {
801+ return badRequest ( 'No data provided' ) ;
802+ }
803+ const { suggestionIds } = context . data ;
804+ if ( ! isArray ( suggestionIds ) || suggestionIds . length === 0 ) {
805+ return badRequest ( 'Request body must contain a non-empty array of suggestionIds' ) ;
806+ }
807+
808+ const site = await Site . findById ( siteId ) ;
809+ if ( ! site ) {
810+ return notFound ( 'Site not found' ) ;
811+ }
812+
813+ if ( ! await accessControlUtil . hasAccess ( site ) ) {
814+ return forbidden ( 'User does not belong to the organization' ) ;
815+ }
816+
817+ const opportunity = await Opportunity . findById ( opportunityId ) ;
818+ if ( ! opportunity || opportunity . getSiteId ( ) !== siteId ) {
819+ return notFound ( 'Opportunity not found' ) ;
820+ }
821+
822+ // Fetch all suggestions for this opportunity
823+ const allSuggestions = await Suggestion . allByOpportunityId ( opportunityId ) ;
824+
825+ // Track valid, failed, and missing suggestions
826+ const validSuggestions = [ ] ;
827+ const failedSuggestions = [ ] ;
828+
829+ // Check each requested suggestion (basic validation only)
830+ suggestionIds . forEach ( ( suggestionId , index ) => {
831+ const suggestion = allSuggestions . find ( ( s ) => s . getId ( ) === suggestionId ) ;
832+
833+ if ( ! suggestion ) {
834+ failedSuggestions . push ( {
835+ uuid : suggestionId ,
836+ index,
837+ message : 'Suggestion not found' ,
838+ statusCode : 404 ,
839+ } ) ;
840+ } else if ( suggestion . getStatus ( ) !== SuggestionModel . STATUSES . NEW ) {
841+ failedSuggestions . push ( {
842+ uuid : suggestionId ,
843+ index,
844+ message : 'Suggestion is not in NEW status' ,
845+ statusCode : 400 ,
846+ } ) ;
847+ } else {
848+ validSuggestions . push ( suggestion ) ;
849+ }
850+ } ) ;
851+
852+ let succeededSuggestions = [ ] ;
853+
854+ // Only attempt deployment if we have valid suggestions
855+ if ( isNonEmptyArray ( validSuggestions ) ) {
856+ try {
857+ const tokowakaClient = TokowakaClient . createFrom ( context ) ;
858+ const deploymentResult = await tokowakaClient . deploySuggestions (
859+ site ,
860+ opportunity ,
861+ validSuggestions ,
862+ ) ;
863+
864+ // Process deployment results
865+ const {
866+ succeededSuggestions : deployedSuggestions ,
867+ failedSuggestions : ineligibleSuggestions ,
868+ } = deploymentResult ;
869+
870+ // Update successfully deployed suggestions with deployment timestamp
871+ const deploymentTimestamp = Date . now ( ) ;
872+ succeededSuggestions = await Promise . all (
873+ deployedSuggestions . map ( async ( suggestion ) => {
874+ const currentData = suggestion . getData ( ) ;
875+ suggestion . setData ( {
876+ ...currentData ,
877+ tokowakaDeployed : deploymentTimestamp ,
878+ } ) ;
879+ suggestion . setUpdatedBy ( 'tokowaka-deployment' ) ;
880+ return suggestion . save ( ) ;
881+ } ) ,
882+ ) ;
883+
884+ // Add ineligible suggestions to failed list
885+ ineligibleSuggestions . forEach ( ( item ) => {
886+ failedSuggestions . push ( {
887+ uuid : item . suggestion . getId ( ) ,
888+ index : suggestionIds . indexOf ( item . suggestion . getId ( ) ) ,
889+ message : item . reason ,
890+ statusCode : 400 ,
891+ } ) ;
892+ } ) ;
893+
894+ context . log . info ( `Successfully deployed ${ succeededSuggestions . length } suggestions to Edge` ) ;
895+ } catch ( error ) {
896+ context . log . error ( `Error deploying to Tokowaka: ${ error . message } ` , error ) ;
897+ // If deployment fails, mark all valid suggestions as failed
898+ validSuggestions . forEach ( ( suggestion ) => {
899+ failedSuggestions . push ( {
900+ uuid : suggestion . getId ( ) ,
901+ index : suggestionIds . indexOf ( suggestion . getId ( ) ) ,
902+ message : 'Deployment failed: Internal server error' ,
903+ statusCode : 500 ,
904+ } ) ;
905+ } ) ;
906+ }
907+ }
908+
909+ const response = {
910+ suggestions : [
911+ ...succeededSuggestions . map ( ( suggestion ) => ( {
912+ uuid : suggestion . getId ( ) ,
913+ index : suggestionIds . indexOf ( suggestion . getId ( ) ) ,
914+ statusCode : 200 ,
915+ suggestion : SuggestionDto . toJSON ( suggestion ) ,
916+ } ) ) ,
917+ ...failedSuggestions ,
918+ ] ,
919+ metadata : {
920+ total : suggestionIds . length ,
921+ success : succeededSuggestions . length ,
922+ failed : failedSuggestions . length ,
923+ } ,
924+ } ;
925+ response . suggestions . sort ( ( a , b ) => a . index - b . index ) ;
926+
927+ return createResponse ( response , 207 ) ;
928+ } ;
929+
781930 return {
782931 autofixSuggestions,
783932 createSuggestions,
933+ deploySuggestionToEdge,
784934 getAllForOpportunity,
785935 getPagedForOpportunity,
786936 getByID,
0 commit comments